Указатель На Функцию Член

Указатель На Функцию Член




🛑 👉🏻👉🏻👉🏻 ИНФОРМАЦИЯ ДОСТУПНА ЗДЕСЬ ЖМИТЕ 👈🏻👈🏻👈🏻





















































Эта статья опубликована в журнале
RSDN Magazine

#6-2004. Информацию о журнале можно найти здесь
В стандарте С++ нет настоящих объектно-ориентированных указателей на функции. Это очень плохо, т.к. объектно-ориентированные указатели на функции, также известные как делегаты, доказали свою значимость в аналогичных языках. В Delphi (Объектный Паскаль) они являются основой библиотеки визуальных компонент (VCL). В большинстве приложений делегаты упрощают использование элегантных паттернов проектирования (Наблюдатель, Стратегия, Состояние [GoF]). Нет никакого сомнения , что такая возможность была бы очень полезна в стандартном С++.
Вместо делегатов С++ предоставляет указатели на функции-члены. Большинство С++-программистов никогда не использовали указатели на функции-члены, и в общем-то, по понятной причине. У этих указателей свой собственный ужасающий синтаксис (операторы ->* и .* , например), по ним трудно найти информацию, и большинство вещей, реализуемых с их помощью, лучше реализуются другими способами. Интересная ситуация: производителю компилятора проще реализовать делегаты, нежели указатели на функции-члены!
В этой статье я приоткрою завесу над указателями на функции-члены. После напоминания о синтаксисе и идеологии указателей на функции-члены, я объясню, как все это реализуется на наиболее популярных компиляторах. Покажу, как компиляторы могут эффективно реализовать делегатов. И наконец, я продемонстрирую, как, используя все эти знания, сделать реализацию оптимально эффективных делегатов. Например, вызов простого делегата на Visual C++ генерирует всего лишь две строчки ассемблерного кода!
Начнем с краткого обзора простых указателей на функции. В С, и С++ в частности, указатель на функцию с именем my_func_ptr , указывающий на функцию, принимающую в качестве аргументов int и char*, и возвращающую float, объявляется так:
Заметьте, что различным комбинациям аргументов соответствуют различные типы указателей на функцию. В MSVC, кроме этого, указатели на функцию различаются в зависимости от типа соглашения о вызове (calling conventions): __cdecl, __stdcall, __fastcall. Для того чтобы указатель на функции указывал на вашу функцию, необходимо выполнить следующую конструкцию:
Для вызова функции через указатель:
Разрешается приводить один тип указателя на функцию к другому. Но не разрешается приводить указатель на функцию к указателю на void. Остальные разрешенные операции тривиальны. Указателю на функцию можно присвоить 0 для обозначения нулевого указателя. Доступны многочисленные операторы сравнения (==, !=, <, >, <=, >=), можно также проверить на равенство 0 либо неявным преобразованием к bool. Кроме того, указатель на функцию может быть использован в качестве нетипизированного параметра шаблона. Он в корне отличается от типизированного параметра, а также отличается от интегрального нетипизированного параметра шаблона. При инстанцировании используется имя, а не тип или значение. Именные параметры шаблонов поддерживаются не всеми компиляторами, даже не всеми из тех, которые поддерживают частичную специализацию шаблонов.
Наиболее распространенное применение указателей на функции в С – это использование библиотечных функций, таких как qsort, и обратных (callback) функций в Windows. Кроме того, есть еще много вариантов их применения. Реализация указателей на функции проста: это всего лишь «указатели на код», в них содержится начальный адрес участка ассемблерного кода. Различные типы указателей существуют лишь для уверенности в корректности применяемого соглашения о вызове.
В программах на С++, большинство функций являются членами. Это означает, что они являются частью класса. И использовать обычный указатель на функцию в этом случае нельзя. Вместо этого нужно использовать указатель на функцию-член. Указатель на функцию-член класса SomeClass, с теми же аргументами, что и ранее, объявляется следующим образом:
Заметьте, что используется специальный оператор ( ::* ), а при объявлении используется класс SomeClass. Указатели на функции-члены имеют очень серьезное ограничение – они могут указывать лишь на функции-члены одного класса. Различным комбинациям аргументов, типам константности и различным классам соответствуют различные указатели на функции-члены. В MSVC , кроме того, указатели различаются по типу соглашения о вызове: __cdecl, __fastcall, __stdcall и __thiscall (__thiscall по умолчанию. Заметим, что документированного квалификатора __thiscall нет, но он иногда появляется в сообщениях об ошибках. Если вы попробуете использовать его явно, то получите сообщение об ошибке, информирующей о том, что использование этого квалификатор зарезервировано для будущих нужд.) При использовании указателей на функции-члены всегда следует использовать typedef, во избежание ошибок и лишних неприятностей.
Чтобы указатель на функцию-член указывал на метод класса SomeClass::some_member_func(int, char*) , нужно выполнить следующую инструкцию:
Многие компиляторы (например, NSVC) разрешат вам пропустить взятие адреса (&), но более соответствующие стандарту (например, GNU G++) потребуют этого. Так что если пишете портируемый код, то не забывайте &. Для вызова метода через указатель на функцию-член нужно предоставить объект SomeClass и воспользоваться специальным оператором (->*). Данный оператор имеет низкий приоритет, так что его следует поместить в скобки:
Прошу меня не винить в ужасном синтаксисе – похоже, кто-то из разработчиков С++ любит знаки препинания!
С++ добавил в С три специальных оператора специально для поддержки указателей на функции-члены. ::* используется при объявлении указателя, ->* и .* используются при вызовах методов, на которые указывает указатель. Похоже, очень много внимания было уделено этой неясной и редко используемой части языка (разрешено даже перегрузить оператор ->*, однако, я не представляю, для чего это может вам потребоваться; я только знаю одно применение [Meyers]).
Указатели на функции-члены могут быть установлены в 0, и предоставляют операторы ==, !=, но лишь для указателей на функции-члены одного класса. Любой указатель на функцию-член может быть проверен на равенство 0. В отличие от простых указателей на функции, операции сравнения на неравенство (<,>, <=, >=) недоступны. Как и простые указатели, они могут быть использованы как нетипизированные параметры шаблона.
Рассмотрим ограничения, накладываемые на указатели на функции-члены. Во-первых, нельзя использовать указатель на функцию-член для статического метода. В этом случае нужно использовать обычный указатель на функцию (так что название «указатель на функцию-член» несколько некорректно, на самом деле это «указатель на нестатическую функцию-член»). Во-вторых, при работе с классами-наследниками есть несколько особенностей. Например, следующий код будет скомпилирован на MSVC, если оставить комментарии:
Довольно любопытно, &DerivedClass::some_member_func являтся указателем на функцию-член класса SomeClass. Это не член класса DerivedClass! Некоторые компиляторы ведут себя несколько иначе: например, Digital Mars C++ считает в данном случае, что &DerivedClass::some_member_func не определен. Но если DerivedClass переопределяет some_member_func, код не будет скомпилирован, т.к. &DerivedClass::some_member_func теперь становится указателем на функцию-член класса DerivedClass!
Приведение между указателями на функции-члены – крайне темная область. Во время стандартизации С++ было много дискуссий по поводу того, разрешено ли приводить указатели на функции-члены одного класса к указателям на функции-члены базового класса или класса-наследника, и можно ли приводить указатели на функции-члены независимых классов. К тому времени, когда комитет по стандартизации определился в этих вопросах, различные производители компиляторов уже сделали свои реализации, причем их ответы на эти вопросы различались. В соответствии со стандартом (секция 5.2.10/9), разрешено использование reinterpret_cast для хранения указателя на член одного класса внутри указателя на член независимого класса. Результат вызова функции в приведенном указателе не определен. Единственное, что можно с ним сделать - это привести его назад к классу, от которого он произошел. Я рассмотрю это далее, т.к. в этой области Стандарт имеет мало сходства с реальными компиляторами.
На некоторых компиляторах происходят ужасные вещи, даже при приведении между указателями на члены базового и наследуемого классов. При множественном наследовании использование reinterpret_cast для приведения указателя на фукнцию-член наследуемого класса к указателю на функцию-член базового класса может скомпилироваться, а может и нет, в зависимости от того в каком порядке базовые классы перечислены в объявлении наследника! Вот пример:
В случае А, static_cast(x) отработает успешно, а static_cast(x) нет. В то время как для случая Б верно противоположное. Вы можете безопасно приводить указатель на функцию член класса-наследника к указателю на функцию-член только первого базового класса! Если вы попробуете все-таки выполнить приведение не к первому базовому классу, MSVC выдаст предупреждение C4407, а Digital Mars C++ выдаст ошибку. Оба будут протестовать против использования reinterpret_cast вместо static_cast, но по разным причинам. Некоторые же компиляторы будут совершенно счастливы, вне зависимости от того, что вы делаете. Будьте осторожны!
Также в Стандарте есть другое интересное правило: можно объявлять указатель на функцию-член класса, до того как этот класс определен. У этого правила есть непредвиденные побочные эффекты, о которых я расскажу позже.
Также стоит отметить, что Стандарт С++ предоставляет указатели на члены-данные. Они имеют те же операторы и некоторые из особенностей реализации указателей на функции-члены. Они используются в некоторых реализациях stl::stable_sort, но я не знаю других значимых применениях этих указателей.
Теперь я, наверное, убедил вас в ужасности указателей на функции-члены. Но чем же они полезны? Пытаясь выяснить это, я провел большой поиск по коду, опубликованному в Интернете. И я нашел два общих случая использования указателей на функции-члены:
Указатели на функции-члены имеют также тривиальное применение в однострочных адаптерах функций в STL и boost, позволяя использование методов в стандартных алгоритмах. В таких случаях они используются во время компиляции. Как правило, на самом деле никаких указателей на функции-члены нет в скомпилированном коде. Наиболее интересное применение указателей на функции-члены – это определение сложных интерфейсов. Этим способом можно реализовать некоторые впечатляющие вещи, но я нашел не так уж много примеров. В большинстве случаев, эти вещи можно выполнить более элегантно при помощи виртуальных функций, или произведя рефакторинг. Наиболее частое применение указатели на функции-члены находят во фреймворках разного типа. Они образуют ядро системы сообщений MFC.
При использовании макросов карт сообщений в MFC (например, ON_COMMAND) на самом деле вы заполняете массив, содержащий идентификатор сообщения (ID) и указатель на функцию-член (а именно, указатели на функции-члены – CCmdTarget::*). Вот почему все классы, которые хотят обрабатывать события, должны быть унаследованы от CCmdTarget. Но различные функции обработки сообщений имеют различный набор аргументов (например, функция-обработчик события OnDraw имеет первым параметром CDC* ), значит, массив должен содержать указатели на функции-члены разных типов. Как это делается в MFC? Они используют ужасный хак, складывая все возможные указатели на функции-члены в огромное объединение (union) для обхода нормальной проверки типов С++ (посмотрите на объединение MessageMapFunctions в файле afximpl.h и cmdtarg.cpp для дополнительной информации). Поскольку MFC – это довольно важная часть многих программ, на практике все С++-компиляторы поддерживают такой хак.
В своих поисках я не смог найти много примеров хорошего использования указателей на функции-члены, кроме как во время компиляции. При всей своей сложности они не добавляют ничего особого в язык. Очень трудно опровергнуть заключение, что в С++ указатели на функции-члены имеют неполноценный дизайн.
При написании этой статьи, я понял что: абсурдно, что Стандарт С++ позволяет приводить типы указателей на функции-члены, но не позволяет вызывать при их помощи функции после приведения. Это абсурдно по трем причинам. Во-первых, приведение не всегда будет работать на многих популярных компиляторах (значит, приведение определено стандартом, но не всегда портируемо). Во-вторых, на всех компиляторах, если приведение произошло удачно, вызов метода через приведенный указатель работает в точности так, как вы думаете: этот вызов не нужно классифицировать как неопределенное поведение (UB) (вызов метода портируем, но не определен стандартом). В-третьих, разрешение приводить указатели на функции-члены без разрешения осуществлять последующий вызов совершенно неприменимо. Но если и приведение, и вызов разрешены, то легко реализовать эффективные делегаты с большой пользой для языка.
Чтобы убедить вас в этом противоречивом утверждении, рассмотрим файл, состоящий лишь из следующего кода. Это правильный код на С++.
Заметьте, что компилятор генерирует ассемблерный код для вызова функции-члена через указатель, ничего не зная о классе SomeClass. Точнее, пока линковщик не выполнит крайне умудренную оптимизацию, код должен корректно работать независимо от настоящего определения класса. Прямое следствие из этого - можно безопасно вызывать метод класса через указатель на функцию-член, приведенный от указателя на функцию-член совершенно другого класса.
Для объяснения второй части моего утверждения, что приведение работает не так, как указано в Стандарте, нужно в деталях рассмотреть, как компиляторы реализуют указатели на функции-члены. Также это поможет объяснить, почему правила использования указателей на функции-члены такие строгие. Трудно найти точную документацию по указателям на функции-члены, много неверной информации, и я исследовал ассемблерный код, генерируемый множеством компиляторов. Итак, пришло время испачкать руки.
Функции-члены классов несколько отличаются от стандартных функций. Кроме обычных параметров они принимают скрытый параметр, называемый this, который указывает на объект класса. В зависимости от компилятора, this может быть внутри обычным указателем, или может приобретать какой-то особый смысл (например, в VC++ this как правило передается через регистр ECX). this отличается от обычных параметров. Для виртуальных функций он в процессе исполнения определяет, какая функция будет выполнена. Даже несмотря на то, что внутри функции-члены – это те же обычные функции, в стандартном С++ нет способа заставить обычную функцию вести себя как функция-член: нет ключевого слова thiscall, которое устанавливает корректное соглашение о вызове.
Вы, возможно, думаете, что указатель на функцию-член, как и обычный указатель на функцию, содержит всего лишь указатель на код. Если так, то вы ошибаетесь. Почти на всех компиляторах указатель на функцию-член больше указателя на функцию. Наиболее ужасно, что в VC++ размер указателя на функцию-член может быть 4, 8, 12 или 16 байтов, в зависимости от природы класса, с которым он ассоциирован, и используемых настроек компилятора! Указатели на функции-члены сложнее, чем вы, возможно, представляете. Но это не всегда было так.
Давайте вернемся назад, в ранние 80-е. Родной компилятор С++ (CFront) поддерживал лишь возможность одиночного наследования. Когда были представлены указатели на функции-члены, они были простыми: они были обычными указателями на функции с дополнительным параметром this в качестве первого аргумента. Когда же появились виртуальные функции, указатели на функции стали указывать на небольшой отрывок дополнительного кода.
Идеальный мир был разрушен с выпуском новой версии CFront 2.0. Новая версия представила шаблоны и множественное наследование. Частью ущерба, причиненного множественным наследованием, стало усечение функциональности указателей на функции-члены. Проблема в том, что при множественном наследовании до тех пор, пока не сделан вызов, неизвестно, какой указатель this использовать. Например, есть четыре класса определенные ниже:
Предположим, мы создаем указатель на функцию-член класса С. В этом примере Afunc и Cfunc являются методами класса С, так что указателю на функцию-член разрешено указывать на Afunc или Cfunc. Но Afunc требует указатель this, указывающий на C::A (который я назову Athis), в то время как Cfunc требует указатель this, указывающий на C (который я назову Cthis). Авторы компиляторов справлялись с этой проблемой при помощи хитрого трюка: они знали, что А физически находится в начале С. Это означает, что Athis == Cthis. Есть лишь один this, о котором нужно заботиться, и все будет хорошо.
Теперь предположим, что мы создаем указатель на функцию-член класса D. В этом случае наш указатель может указывать на Afunc, Bfunc или Dfunc. Но Afunc требует указатель this, указывающий на D::A, в то время как Bfunc нужен указатель this, указывающий на D::B. Теперь предыдущий трюк нельзя использовать. Нельзя положить оба класса, A и B, в начало D. Поэтому указатель на функцию-член класса D должен определять не только какую функцию вызывать, но и какой указатель this использовать. Компилятор знает размер класса А, так что он сможет преобразовать указатель Athis в указатель Bthis, всего лишь добавив к нему смещение (delta = sizeof(A)).
При использовании виртуального наследования (виртуальных базовых классов) все становится намного хуже, и легко запутаться, пытаясь понять механизм работы. Как правило, компилятор использует таблицу виртуальных функций (vtable) в которой для каждой виртуальной функции хранится адрес функции и virtual_delta: количество байт, которые нужно добавить к указателю this , чтобы получить нужный указатель this, требуемый функции.
Ни одной из этих сложностей не было бы, если бы в С++ указатели на функции-члены были определены несколько иначе. В приведенном выше коде сложность появляется из-за того, что разрешено ссылаться на A::Afunc как на D::Afunc. Вероятно, это плохой стиль. Обычно следует использовать базовые классы как интерфейсы. Если бы вы делали только так, указатели на функции члены были бы обычными указателями на функции со специальным соглашением о вызове. На мой взгляд, разрешение указывать на переопределенные функции было трагической ошибкой. Из-за этой редко используемой функциональности указатели на функции-члены стали нелепицей. Кроме того, они причиняют головную боль вынужденным реализовать их авторам компиляторов.
Итак, как же компиляторы реализуют указатели на функции-члены? В таблице приведены результаты применения оператора sizeof к различным структурам (int, указатель на данные void*, указатель на код (т.е. указатель на статическую функцию), и указатель на функцию-член класса с одиночным, множественным, виртуальным наследованиями, или неопределенного
http://rsdn.org/article/cpp/fastdelegate.xml
https://www.cyberforum.ru/cpp-beginners/thread951.html
Смотреть Секс Фильмы Со Зрелыми Женщинами
Федорова Фото Ню
Секс Милое Пони
Указатели на функции-члены и реализация самых быстрых ...
Указатель на функцию член - C++ - Киберфорум
Указатель функции на функцию-член
Указатели на функции-члены; почему сделали именно так? - …
указатель на функцию-член, который возвращает тот же тип ...
Как передать указатель на функцию - член класса? - C/C++ ...
С++ Указатель На Функцию-Член
Указатель на функцию-член - techfeed.net
Указатель На Функцию Член


Report Page