Найти в Дзене
coding style

C++ Указатель на метод класса в вопросах и ответах

Да. Рассмотрим следующую функцию: Тип этой функции зависит от того, является ли она обычной функцией или нестатической функцией-членом некоторого класса: Примечание: если это статическая функция-член класса Fred, то ее тип такой же, как если бы это была обычная функция: «int (*)(char,float)». Никак. Поскольку функция-член бессмысленна без объекта, на котором она вызывается, вы не можете сделать это напрямую (если бы The X Window System была переписана на C++, она, вероятно, передавала бы ссылки на объекты, а не только указатели на функции; естественно, объекты воплощали бы требуемую функцию и, вероятно, многое другое). В качестве исправления для существующего кода используйте функцию верхнего уровня (не являющуюся членом) в качестве обертки, которая принимает объект, полученный каким-либо другим способом. В зависимости от вызываемой вами функции, этот «другой способ» может быть элементарным или потребовать небольшой работы с вашей стороны. Например, системный вызов, запускающий поток,
Оглавление

Отличаются ли типы «указатель на функцию член класса» и «указатель на функцию»?

Да. Рассмотрим следующую функцию:

-2

Тип этой функции зависит от того, является ли она обычной функцией или нестатической функцией-членом некоторого класса:

  • Если это обычная функция, то ее тип «int (*)(char,float)»
  • Если это нестатическая функция-член класса Fred, то ее тип «int (Fred::*)(char,float)»

Примечание: если это статическая функция-член класса Fred, то ее тип такой же, как если бы это была обычная функция: «int (*)(char,float)».

Как передать указатель на метод класса в обработчик сигнала Unix, обратный вызов события X Window System или system call запускающий поток, и т. д.?

Никак. Поскольку функция-член бессмысленна без объекта, на котором она вызывается, вы не можете сделать это напрямую (если бы The X Window System была переписана на C++, она, вероятно, передавала бы ссылки на объекты, а не только указатели на функции; естественно, объекты воплощали бы требуемую функцию и, вероятно, многое другое).

В качестве исправления для существующего кода используйте функцию верхнего уровня (не являющуюся членом) в качестве обертки, которая принимает объект, полученный каким-либо другим способом. В зависимости от вызываемой вами функции, этот «другой способ» может быть элементарным или потребовать небольшой работы с вашей стороны. Например, системный вызов, запускающий поток, может потребовать передать два параметра: указатель на функцию и void*, так что вы можете передать указатель на объект класса в виде void*. Многие операционные системы делают нечто подобное для функции, запускающей новую задачу. В худшем случае вы можете хранить указатель на объект класса в глобальной переменной; это может потребоваться для обработчиков сигналов Unix (но глобальные переменные, в общем случае, нежелательны). В любом случае функция верхнего уровня будет вызывать нужную функцию-член на объекте.

Вот пример простейшего и худшего сценария (использование глобальной переменной). Предположим, вы хотите вызвать Fred::memberFn() по прерыванию:

-3

Примечание: для вызова статических функций-членов не требуется фактический объект, поэтому указатели на статические функции-члены обычно совместимы по типу с обычными указателями на функции. Однако, хотя это, вероятно, работает в большинстве компиляторов, на самом деле для корректности нужно использовать объявления extern "C", поскольку линковщик C покрывает не только такие вещи, как манглирование (искажение) имен, но и соглашения о вызове, которые могут отличаться между C и C++.

Почему я постоянно получаю ошибки компиляции (несоответствие типов), когда пытаюсь использовать функцию-член в качестве подпрограммы обслуживания прерываний?

Это частный случай двух предыдущих вопросов. Нестатические функции-члены имеют скрытый параметр, который соответствует указателю this. Указатель this указывает на экземпляр объекта. Аппаратное/программное обеспечение прерываний в системе не способно передать аргумент this. Поэтому вы должны использовать «обычные» функции (не члены класса) или статические функции-члены в качестве процедур обслуживания прерываний.

Одно из возможных решений - использовать статическую функцию-член в качестве процедуры обслуживания прерывания и заставить эту функцию искать где-нибудь пару экземпляр/член, которая должна быть вызвана при прерывании. Таким образом, получится, что функция-член вызывается при прерывании, но по техническим причинам сначала нужно вызвать промежуточную функцию.

Почему у меня возникают проблемы с получением адреса функции члена класса C++?

В C++ функции-члены имеют неявный параметр, который указывает на объект (указатель this внутри функции-члена). Можно считать, что обычные функции C имеют отличный от функций-членов механизм вызова, поэтому типы их указателей различны и несовместимы. C++ вводит новый тип указателя, называемый pointer-to-member, который может быть вызван только путем предоставления объекта.

ПРИМЕЧАНИЕ: не пытайтесь приводить типы указатель на функцию-член класса и указатель на обычную функцию; результат будет неопределенным и катастрофическим. Как было сказано в предыдущем разделе, используйте в качестве промежуточного звена либо обычную функцию языка Си, либо статическую функцию-член класса.

Как избежать синтаксических ошибок при создании указателей на функции-члены?

Используйте typedef или using.

Да, есть множество примеров когда разработчики отказывались от этого механизма, но потом тратили часы и часы своего времени, в то время как 10 секунд работы с typedef упростили бы им жизнь. Кроме того, признайте, что вы пишете не тот код, который сможете читать только вы; вы, надеюсь, пишете код, который смогут прочитать и другие. Так зачем же намеренно усложнять жизнь себе и другим? Будьте умнее: используйте typedef.

Вот пример класса:

-4

using или typedef тривиален:

-5

Вот и все! FredMemFn - это тип указатель, который указывает на любой член Fred, принимающий параметры (char,float), например, Fred'овские f, g, h и i.

Затем можно тривиально объявить указатель на функцию-член:

-6

Также тривиально объявлять новые функции, которые получают указатели на функции-члены в параметрах:

-7

Также тривиально объявлять функции, возвращающие указатели на функции-члены:

-8

Как избежать синтаксических ошибок при вызове функции-члена с помощью указателя?

Если у вас есть доступ к компилятору стандарта C++17, используйте std::invoke. В противном случае используйте макрос:

-9

Причина, по которой std::invoke или этот макрос являются хорошей идеей, заключается в том, что вызовы функций-членов часто намного сложнее, чем приведенный ниже простой пример. Разница в удобстве чтения и написания очень существенна. Не добавляйте ненужной сложности в свой код.

Пример std::invoke

-10

Пример использования макроса

-11

Пример без std::invoke или макроса

-12

Как создать и использовать массив указателей на функции-члены?

Сначала используйте техники из двух предыдущих разделов

Шаг 1: создайте псевдоним типа

-13

Шаг 2: создайте макрос, если нет наличия std::invoke

-14

Шаг 3: объявите массив

-15

Шаг 4: используйте массив для вызова мотодов класса

-16

или:

-17

Как создать указатель на константную функцию-член класса

Просто добавьте ключевое слово const к объявлению

-18

Далее просто используйте FredMemFn как обычный тип

-19

Какая разница между операторами .* и ->*

Вам не нужно это понимать, если вы используете std::invoke или макрос для вызовов функций-членов по указателю. Но если вы действительно хотите избежать std::invoke или макроса, используйте .* когда левый аргумент является ссылкой на объект, и ->* когда он является указателем на объект.

Например:

-20

Но, пожалуйста, рассмотрите возможность использования std::invoke

-21

или макроса

-22

Как уже говорилось ранее, реальные вызовы часто намного сложнее, чем простые, приведенные здесь, поэтому использование std::invoke или макросов обычно улучшает удобство записи и чтения кода.

Можно ли преобразовать указатель на функцию-член в void*

Нет

-23

Технические детали: указатели на функции-члены и указатели на данные не обязательно реализуются одинаково. Указатель на функцию-член может быть структурой данных, а не одним указателем. Подумайте об этом: если он указывает на виртуальную функцию, то на самом деле он может указывать не на статический блок кода, так что это может быть даже не обычный адрес - это может быть неизвестная структура данных. Даже если выше приведенный код работает на вашей конкретной версии вашего конкретного компилятора на вашей конкретной операционной системе, он не актуален в условиях другого окружения.

Можно ли преобразовать указатель на функцию в void*

Нет

-24

Технические детали: указатели void* - это указатели на данные, а указатели функций - на функции. Стандарт не требует, чтобы функции и данные находились в одном адресном пространстве, поэтому (в качестве примера, а не ограничения) на архитектурах где они находятся в разных адресных пространствах, два разных типа указателей будут несопоставимы.

Мне нужно что-то вроде указателя на функцию, но с большей гибкостью и/или потокобезопасностью; есть ли другой способ?

Используйте функциоид.

Что такое функциоид и зачем он мне нужен?

Функциоиды - это функции на стероидах. Функциоиды строго мощнее функций, и эта дополнительная мощь решает некоторые (не все) проблемы, с которыми обычно сталкиваются при использовании указателей на функции. Давайте рассмотрим пример традиционного использования указателей, а затем переведем этот пример в функциоиды. Традиционная идея использования указателей состоит в том, чтобы иметь кучу совместимых по сигнатуре функций:

-25

Затем вы обращаетесь к ним с помощью указателей:

-26

Иногда люди создают массив этих указателей:

-27

В этом случае они вызывают функцию, обращаясь к элементу массива:

-28

При использовании функциоидов сначала создается базовый класс с чисто виртуальным методом:

-29

Затем вместо трех функций вы создаете три производных класса:

-30

Затем вместо передачи указателя функции вы передаете указатель на базовый класс. Я создам typedef под названием FunctPtr просто для того, чтобы сделать остальной код похожим на старый подход:

-31

Почти таким же образом можно создать их массив:

-32

Это дает нам первый намек на то, что функциоиды строго мощнее, чем указатели функций: тот факт, что в подходе функциоидов есть аргументы, которые можно передавать конструкторам (показанные выше как ...ctor-args...), в то время как в версии указателей функций их нет. Думайте о функциоидном объекте как о замороженной функции-вызове (акцент на слове «вызов»). В отличие от указателя на функцию, функциоид - это (концептуально) указатель на частично вызванную функцию. Представьте на минуту технологию, которая позволяет вам передать функции некоторые, но не все аргументы, а затем заморозить этот (частично завершенный) вызов. Представьте, что эта технология возвращает вам некий магический указатель на эту замороженную частично завершенную функцию-вызов. Затем вы передаете оставшиеся аргументы, используя этот указатель, и система волшебным образом берет ваши исходные аргументы (которые были заморожены), объединяет их с любыми локальными переменными, которые функция вычислила до заморозки, объединяет все это с вновь переданными аргументами и продолжает выполнение функции с того места, где она остановилась после заморозки. Это может показаться научной фантастикой, но концептуально функциоиды позволяют вам это делать. Кроме того, они позволяют многократно «завершать» вызов замороженной функции с различными «оставшимися параметрами», сколько угодно раз. Плюс они позволяют (не требуют) изменять состояние замороженной функции при ее вызове, а значит, функциоиды могут запоминать информацию от одного вызова к другому.

Давайте вернемся на землю и на паре примеров объясним, что на самом деле означает вся эта муть.

Предположим, что исходные функции (в старомодном стиле указателей функций) принимали немного другие параметры.

-33

Когда параметры различны, старомодный подход с использованием указателей функций затруднителен, поскольку вызывающая сторона не знает, какие параметры передавать (вызывающая сторона просто имеет указатель на функцию, а не имя функции или, если параметры различны, количество и их типы) (не пишите мне о том что вы можете это сделать, для этого вам придется встать на голову и делать грязные вещи; используйте вместо этого функциоиды).

С функциоидами ситуация, по крайней мере иногда, гораздо лучше. Поскольку функциоид можно рассматривать как замороженный вызов функции, просто возьмите нестандартные аргументы, например те, которые я назвал y и/или z, и сделайте их аргументами соответствующих конструкторов. Вы также можете передать общие аргументы (в данном случае int с именем x) в конструктор, но это не обязательно - у вас есть возможность передать их в чистый виртуальный метод doit(). Я предположу, что вы хотите передать x в doit(), а y и/или z - в конструктор:

-34

Тогда вместо трех функций вы создадите три производных класса:

-35

Теперь вы видите, что параметры конструктора замораживаются в функциоиде, когда вы создаете массив функциоидов:

-36

Таким образом, когда пользователь вызывает doit() для одного из этих функциоидов, он передает «оставшиеся» аргументы, и вызов концептуально объединяет исходные аргументы, переданные конструктору, с теми, что были переданы в метод doit():

-37

Как я уже намекал, одно из преимуществ функциоидов в том, что в вашем массиве может быть несколько экземпляров, скажем, Funct1, и эти экземпляры могут иметь различные параметры, замороженные в них. Например, array[0] и array[1] оба имеют тип Funct1, но поведение array[0]->doit(12) будет отличаться от поведения array[1]->doit(12), поскольку поведение будет зависеть как от 12, переданных в doit(), так и от аргументов, переданных в конструктор.

Еще одно преимущество функциоидов становится очевидным, если мы изменим пример с массивом функциоидов на локальный функциоид. Чтобы подготовить сцену, давайте вернемся к старомодному подходу с использованием указателей на функции и представим, что вы пытаетесь передать функцию сравнения в sort() или binarySearch(). Подпрограмма sort() или binarySearch() называется childRoutine(), а тип указателя функции сравнения - FunctPtr:

-38

Тогда разные вызывающие передавали бы разные указатели в зависимости от того, что они считают лучшим:

-39

Мы можем легко преобразовать этот пример в пример с использованием функциоидов:

-40

Учитывая этот пример, мы можем увидеть два преимущества функциоидов перед указателями функций. Преимущество «ctor args», описанное выше, плюс тот факт, что функциоиды могут сохранять состояние между вызовами потокобезопасным образом. При использовании обычных указателей люди обычно сохраняют состояние между вызовами с помощью статических переменных. Однако статические данные по своей сути не являются потокобезопасными - статические данные разделяются между всеми потоками. Подход с использованием функциоидов позволяет получить нечто, что по своей сути является потокобезопасным, поскольку код в конечном итоге получает потоко-локальные данные. Реализация тривиальна: замените старомодные статические переменные на член класса внутри объекта функциоида, и вот, данные не только потокобезопасны, но даже безопасны при рекурсивных вызовах: каждый вызов yourCaller() будет иметь свой собственный объект класса Funct3 со своими собственными данными для каждого экземпляра класса. Обратите внимание, что мы что-то приобрели, ничего не потеряв. Если вам нужны потоко-глобальные данные, функциоиды могут дать вам и это: просто сделайте поля класса внутри объекта функциоида статическими.

Функциоид дает вам третью возможность, которая недоступна при старом подходе: функциоид позволяет вызывающим пользователям решать, нужны ли им потоко-локальные или потоко-глобальные данные. Они будут обязаны использовать блокировки в тех случаях, когда им нужны глобальные данные, но, по крайней мере, у них будет выбор. Это просто:

-41

Функциоиды не решают всех проблем, возникающих при создании гибкого программного обеспечения, но они значительно мощнее, чем указатели на функции, и их стоит хотя бы оценить. На самом деле легко доказать, что функциоиды ничуть не проигрывают в мощности по сравнению с указателями функций, поскольку вы можете представить, что старомодный подход с указателями функций эквивалентен наличию глобального объекта функциоида.

Можно ли сделать функциоиды быстрее, чем обычные вызовы функций?

Да. Если у вас небольшой функциоид, а в реальном мире это довольно часто встречается, стоимость вызова функции может быть высокой по сравнению со стоимостью работы, выполняемой функциоидом. В предыдущем разделе функциоиды реализовывались с помощью виртуальных функций и, как правило, обходились вызовом функции. Альтернативный подход использует шаблоны.

Следующий пример похож по духу на предыдущий. Я переименовал doit() в operator()(), чтобы улучшить читаемость вызывающего кода и позволить комулибо передавать указатель на обычную функцию:

-42

Разница между этим подходом и подходом из предыдущего примера заключается в том, что функциоид «привязывается» к вызывающей стороне во время компиляции, а не во время выполнения. Думайте об этом как о передаче параметра: если вы знаете на этапе компиляции, какой функциоид вы хотите передать, то вы можете использовать описанную выше технику, и вы можете, по крайней мере в типичных случаях, получить выигрыш в скорости за счет того, что компилятор встраивает код функциоида в точку вызова. Вот пример:

-43

Вот один из вариантов вызова вышеприведенной функции:

-44

Примечание: как было сказано в первом абзаце выше, вы также можете передавать имена обычных функций (хотя вы можете понести затраты на вызов функции, когда вызывающая сторона использует их):

-45

В чем разница между функциоидом и функтором?

Функциоид - это объект, имеющий один основной метод. По сути, это OOП расширение C-подобной функции, такой как printf(). Функциоид используется в тех случаях, когда функция имеет более одной точки входа (т. е. более одного «метода») и/или должна сохранять состояние между вызовами потоко-безопасным способом (подход в стиле C для сохранения состояния между вызовами заключается в добавлении локальной статической переменной в функцию, но это ужасно небезопасно в многопоточной среде).

Функтор - это частный случай функциоида: это функциоид, метод которого является «оператором круглые скобки», operator()(). Поскольку он перегружает оператор вызова функции, код может вызывать его основной метод, используя тот же синтаксис, что и для вызова обычной функции. Например, если «foo» - это функтор, то для вызова метода «operator()()» на объекте «foo» нужно написать «foo()». Преимущество этого заключается в использовании шаблонов, поскольку тогда параметр шаблона может быть либо именем функции, либо объектом-функтором, а также в производительности, так как метод «operator()()» может быть встроен компилятором в точку вызова.

Это очень полезно для таких вещей, как функция «сравнение» для отсортированных контейнеров. В C функция сравнения всегда передается по указателю (например, см. сигнатуру к «qsort()»), но в C++ параметр может передаваться либо как указатель на функцию, либо как имя объекта-функтора, и в результате сортированные контейнеры в C++ в некоторых случаях могут быть намного быстрее (и никогда медленнее), чем эквивалент в C.

Поскольку в Java нет ничего похожего на шаблоны, для всего этого приходится использовать динамическое связывание, а динамическое связывание обязательно означает вызов функции по указателю. Обычно это не так важно, но в C++ мы хотим обеспечить чрезвычайно высокую производительность кода. Иными словами, C++ придерживается философии «плати только за то что используешь», что означает, что язык никогда не должен произвольно навязывать накладные расходы по сравнению с тем, что способна выполнить физическая машина (конечно, программист может по желанию использовать такие техники, как динамическое связывание, которые, в общем случае, будут давать накладные расходы в обмен на гибкость или другие «удобства», но это дело разработчика и программиста - решать, нужны ли преимущества (и затраты) таких конструкций).

Источник: isocpp.org