указатель на функцию в качестве параметра c
Указатели на функции
Указатели на функции
К ак уже обсуждалось ранее функции – это набор команд, которые расположены в соответствующей области памяти. Вызов функции – это сохранение состояния, передача аргументов и переход по адресу, где располагается функция. В си есть возможность создавать указатели на функции. Указатели на функции позволяют упростить решение многих задач. Совместно с void указателями можно создавать функции общего назначения (например, сортировки и поиска). Указатели позволяют создавать функции высших порядков (функции, принимающие в качестве аргументов функции): отображение, свёртки, фильтры и пр. Указатели на функции позволяют уменьшать цикломатическую сложность программ, создавать легко масштабируемые конструкции. Рассмотрим пример. Создадим функцию и указатель на эту функцию
Синтаксис объявления указателей на функцию
(* )( );
В этом примере мы создали функцию отображения, которая применяет ко всем элементам массива функцию, которая передаётся ей в качестве аргумента. Когда мы вызываем функцию map, достаточно просто передавать имена функций (они подменяются указателями). Запишем теперь функцию map, которая получает в качестве аргумента массив типа void:
Вот где нам понадобились указатели типа void. Так как map получает указатель на функцию, то все функции должны иметь одинаковые аргументы и возвращать один и тот же тип. Но аргументы функций должны быть разного типа, поэтому мы делаем их типа void. Функция map получает указатель типа void (*)(void*), поэтому ей теперь можно передавать любую из четырёх функций.
Пример другой функции: функция filter получает указатель на массив и возвращает размер нового массива, оставляя в нём только те элементы, для которых переданный предикат возвращает логическую истину (предикат – функция, которая возвращает истину или ложь). Сначала напишем для массива типа int:
Теперь для массива типа void
Ещё одна функция – свёртка. Она получает в качестве аргументов массив и функцию от двух аргументов. Эта функция действует следующим образом: сначала она применяется к первым двум аргументам, затем она применяется к третьему аргументу и результату предыдущего вызова, затем к четвёртому аргументу и результату предыдущего вызова и т.д. С помощью свёртки можно, например, найти сумму всех элементов массива, максимальный элемент массива, факториал числа и т.п.
Последний пример: функция сортировки вставками для массива типа void. Так как тип массива не известен, то необходимо передавать функцию сравнения.
Массив указателей на функции
М ассив указателей на функции определяется точно также, как и обычный массив – с помощью квадратных скобок после имени:
Точно также можно было создать массив динамически
Часто указатели на функцию становятся громоздкими. Работу с ними можно упростить, если ввести новый тип. Предыдущий пример можно переписать так
Ещё один пример: функция any возвращает 1, если в переданном массиве содержится хотя бы один элемент, удовлетворяющий условию pred и 0 в противном случае.
qsort и bsearch
Функция bsearch проводит бинарный поиск в отсортированном массиве и получает указатель на функцию сравнения, такую же, как и функция qsort. В случае, если элемент найден, то она возвращает указатель на этот элемент, если элемент не найден, то NULL.
Указатель на функцию в качестве параметра функции
Помощь в написании контрольных, курсовых и дипломных работ здесь.
Указатель на функцию, которая принимает в качестве параметра указатель на массив
я не понимаю. вроде делаю правильно, но выходит ошибка. есть функция. int foo(int *mas)<>;.
Указатель на файловый поток в качестве параметра функции
Здравствуйте! Хочется/нужно написать что-то вроде функции которая читала бы из файлового потока.
Передача функции в качестве параметра в другую функцию
Задание стоит следующее: Написать программу, содержащую две функции. Первая функция, вычисляющая.
Можно ли в качестве параметра функции передать другую функцию?
В ниже представленной программе функция returnMax находит в массиве максимальный элемент и.
Решение
Помощь в написании контрольных, курсовых и дипломных работ здесь.
Как передать функции указатель на функцию в качестве аргумента?
как передать функции указатель на функцию в качестве аргумента?
Нужно передать в качестве параметра указатель на массив
Нужно передать в качестве параметра указатель на массив. Я так понимаю, есть несколько способов это.
Указатель на объект в качестве параметра вызываемого метода класса
Код по сути //Interface.cpp #include «Interface.h» void Interface::input() < Teplitsa *kaka;.
Указатель на функцию в C
В C, как и обычные указатели данных (int *, char * и т. Д.), У нас могут быть указатели на функции. Ниже приведен простой пример, который показывает объявление и вызов функции с использованием указателя на функцию.
#include
// Нормальная функция с параметром int
// и возвращаемый тип void
/ * Приведенная выше строка эквивалентна следующим двум
// Вызываем fun () используя fun_ptr
Зачем нам нужна дополнительная скобка вокруг указателей на функции, таких как fun_ptr в приведенном выше примере?
Если убрать скобку, то выражение «void (* fun_ptr) (int)» становится «void * fun_ptr (int)», которое является объявлением функции, возвращающей указатель void. Смотрите следующий пост для деталей.
Как объявить указатель на функцию?
Ниже приведены некоторые интересные факты о указателях функций.
1) В отличие от обычных указателей, указатель на функцию указывает на код, а не на данные. Обычно указатель функции хранит начало исполняемого кода.
2) В отличие от обычных указателей, мы не выделяем выделенную память с помощью указателей на функции.
3) Имя функции также может быть использовано для получения адреса функции. Например, в приведенной ниже программе мы удалили адрес оператора ‘&’ в присваивании. Мы также изменили вызов функции, удалив *, программа все еще работает.
#include
// Нормальная функция с параметром int
// и возвращаемый тип void
void (*fun_ptr)( int ) = fun; // & удалено
4) Как и обычные указатели, мы можем иметь массив указателей на функции. Ниже пример в пункте 5 показывает синтаксис для массива указателей.
5) Вместо корпуса переключателя можно использовать указатель функции. Например, в приведенной ниже программе пользователю предлагается выбрать между 0 и 2 для выполнения различных задач.
void add( int a, int b)
void subtract( int a, int b)
void multiply( int a, int b)
unsigned int ch, a = 15, b = 10;
printf ( «Enter Choice: 0 for add, 1 for subtract and 2 «
6) Как и обычные указатели данных, указатель функции может быть передан в качестве аргумента, а также может быть возвращен из функции.
Например, рассмотрим следующую C-программу, где wrapper () получает void fun () в качестве параметра и вызывает переданную функцию.
// Простая программа на C для отображения указателей на функции в качестве параметра
#include
// Две простые функции
// Функция, которая получает простую функцию
// как параметр и вызывает функцию
void wrapper( void (*fun)())
Этот момент особенно полезен в Си. В Си мы можем использовать указатели на функции, чтобы избежать избыточности кода. Например, простая функция qsort () может использоваться для сортировки массивов в порядке возрастания или убывания, или в любом другом порядке в случае массива структур. Кроме того, с помощью указателей на функции и указателей на пустоту можно использовать qsort для любого типа данных.
// Пример для qsort и компаратора
#include
#include
// Пример функции сравнения
// для сортировки целочисленного массива в порядке возрастания.
// Сортировать любой массив для любого другого типа данных и / или
// критерии, все, что нам нужно сделать, это написать больше сравнить
// функции. И мы можем использовать тот же qsort ()
int compare ( const void * a, const void * b)
int n = sizeof (arr)/ sizeof (arr[0]), i;
qsort (arr, n, sizeof ( int ), compare);
Подобно qsort (), мы можем написать свои собственные функции, которые можно использовать для любого типа данных и выполнять различные задачи без избыточности кода. Ниже приведен пример функции поиска, которую можно использовать для любого типа данных. Фактически мы можем использовать эту функцию поиска, чтобы найти близкие элементы (ниже порога), написав настроенную функцию сравнения.
// Функция сравнения, используемая для поиска целого числа
// массив
bool compare ( const void * a, const void * b)
return ( *( int *)a == *( int *)b );
int search( void *arr, int arr_size, int ele_size, void *x,
// Поскольку char занимает один байт, мы можем использовать указатель на char
// для любого типа / чтобы получить правильную арифметику указателя,
// нам нужно умножить индекс на размер массива
Указатели и ссылки
Кувшинов Д.Р.
Ссылки
Ссылка reference — механизм языка программирования (C++), позволяющий привязать имя к значению. В частности, ссылка позволяет дать дополнительное имя переменной и передавать в функции сами переменные, а не значения переменных.
Синтаксически ссылка оформляется добавлением знака & (амперсанд) после имени типа. Ссылка на ссылку невозможна.
Любые действия со ссылкой трактуются компилятором как действия, которые будут выполняться над объектом, к которому эта ссылка привязана. Следующий пример демонстрирует ссылку в качестве дополнительного имени переменной.
Казалось бы, зачем нам второе имя переменной? Ответа может быть, по крайней мере, два.
Впрочем, основным применением ссылок является передача параметров в функции “по ссылке” и возвращение функциями ссылок на некие внешние объекты.
Передача по ссылке by reference напоминает передачу “по имени”. Таким образом, можно сказать, что, используя ссылки, мы передаём не значения, а сами переменные, содержащие эти значения. В реальности “за ширмой” происходит передача адресов этих переменных. Передача ссылки на переменную, время жизни которой заканчивается, например, возврат из функции ссылки на локальную переменную, приводит к неопределённому поведению.
Ранний пример использования ссылок для возврата из функции более одного значения представлен в самостоятельной работе 3.
Приведём здесь ещё один пример: функцию, которая возвращает одну из двух переменных, содержащую максимальное значение. Для этого модифицируем предыдущий пример:
Так как при передаче ссылки реально копируется лишь адрес значения, а не само значение, то передав ссылку можно избежать копирования значения. Поэтому ссылки широко используются для передачи в функцию аргументов, которые или запрещено копировать или вычислительно дорого копировать. Типичный пример — объекты string. При копировании строки происходит выделение динамической памяти, копирование всех символов, затем — при удалении этой копии — освобождение памяти. Часто нет никакой необходимости в копировании. Например, следующей функции, считающей количество повторений заданного символа в строке нет нужды копировать строку — можно обойтись ссылкой:
Ставить слово const можно перед именем типа и после имени типа, это эквивалентные записи.
Указатели
Общие сведения
Что такое указатель pointer уже рассказывалось во введении.
В C и C++ указатель определяется с помощью символа * после типа данных, на которые этот указатель будет указывать.
Указатель — старший родственник ссылки. Указатели активно использовались ещё в машинных языках и оттуда были перенесены в C. Ссылки же доступны только в C++.
Указатели можно сравнивать друг с другом. Указатели равны, если указывают на один и тот же объект, и не равны в противном случае.
Указатели можно передавать в функции и возвращать из функций как и любые “элементарные” значения. Ещё пример с указателями:
Соответственно, ограничения, накладываемые на ссылки по сравнению с указателями, позволяют, с одной стороны, защитить программиста от ряда ошибок, и, с другой стороны, открывают ряд возможностей оптимизации кода для компилятора. Ссылки используются там, где нет нужды в “полноценных” указателях или есть желание не перегружать код взятиями адреса и разыменованиями.
есть то же самое, что
есть то же самое, что
Например, поиск самого левого нуля в массиве чисел с плавающей точкой может быть записан так:
Данный пример использует арифметику указателей и массивы. Данная тема освещена в разделе массивы и ссылки.
Бестиповый указатель
В C бестиповые указатели широко применяются для оперирования кусками памяти или реализации обобщённых функций, которые могут работать со значениями разных типов. В последнем случае конкретный тип маскируется с помощью void (“пустышка”). При использовании таких функций обычно приходится где-то явно приводить тип указателей. C++ позволяет отказаться от подобной практики благодаря поддержке полиморфизма и обобщённого программирования (материал 2-го семестра).
О цикле for (int byte: buffer) см. здесь.
Указатель на указатель
Так как указатель — обычная переменная, возможен указатель на указатель. И указатель на указатель на указатель. И указатель (на указатель) n раз для натурального n. Максимальный уровень вложенности задаётся компилятором, но на практике уровни больше 2 практически не используются.
“Система ранжирования C-программистов.
Чем выше уровень косвенности ваших указателей (т. е. чем больше “*” перед вашими переменными), тем выше ваша репутация. Беззвёздочных C-программистов практически не бывает, так как практически все нетривиальные программы требуют использования указателей. Большинство являются однозвёздочными программистами. В старые времена (ну хорошо, я молод, поэтому это старые времена на мой взгляд) тот, кто случайно сталкивался с кодом, созданный трёхзвёздочным программистом, приходил в благоговейный трепет.
Некоторые даже утверждали, что видели трёхзвёздочный код, в котором указатели на функции применялись более чем на одном уровне косвенности. Как по мне, так эти рассказы столь же правдивы, сколь рассказы об НЛО.
Просто чтобы было ясно: если вас назвали Трёхзвёздочным Программистом, то обычно это не комплимент.«
Условия для проверки себя на “трёхзвёздность” перечислены на другой странице того же сайта.
В случае C указатели на указатели (уровень косвенности 2) используются довольно часто, например, для возвращения указателя из функции, которая возвращает ещё что-то, или для организации двумерных массивов. Пример такой функции из Windows API:
Функция принимает имя файла как указатель на си-строку lpFileName, а также размер буфера nBufferLength в символах и адрес буфера lpBuffer, куда записывается в виде си-строки полное имя файла. Функция возвращает длину строки, записанной в буфер, или 0, если произошла ошибка. Кроме того, последний параметр функции — указатель на указатель на си-строку lpFilePart, который используется, чтобы вернуть из функции указатель на последнюю часть имени файла, записанного в буфер.
В случае C++ с помощью ссылок и Стандартной библиотеки можно вообще избежать использования “классических” указателей. Так что “беззвёздочный” C++-программист возможен.
Неограниченный уровень косвенности
Следующий пример демонстрирует использование связанного списка для чтения последовательности строк и вывода этой последовательности в обратном порядке:
Упражнение. Попробуйте изменить этот пример так, чтобы введённые строки выводились в том же порядке, в котором были введены.
Указатели на функции
Язык C позволяет определять указатели на функции (в указателе хранится адрес точки входа в функцию) и вызывать функции по указателю. Таким образом, можно во время исполнения программы выбирать какая именно функция будет вызвана в конкретной точке, выбирая значение указателя. Язык C++ позволяет создавать также и ссылки на функции, но ввиду того, что ссылка после инициализации не может быть изменена, область применения ссылок на функции весьма узка.
Функцией высшего порядка higher order function называют функцию, принимающую в качестве параметров другие функции. Функции высшего порядка — одно из базовых понятий функционального программирования. Единственная форма функций высшего порядка в C — функции, принимающие указатели на функции. Язык C++ расширяет круг доступных форм функций высшего порядка, но в примерах ниже мы ограничимся возможностями C.
В качестве простого примера применения функции обратного вызова рассмотрим функцию, занимающуюся поиском набора корней уравнения f(x) = 0 на заданном отрезке. Сама функция будет работать по достаточно простому алгоритму (который, естественно, не гарантирует, что будут найдены все или даже какие-то из существующих на отрезке корней): предполагаем, что есть некая функция, способная найти один корень на отрезке, если он там есть (например, функция nsolve из примера выше). Теперь берём исходный отрезок поиска [a, b] и некоторое значение “шага” step и проходим по этому отрезку с этим шагом, проверяя участки [a + i step, min(b, a + (i + 1)step], i = 0, … пока не пересечём правую границу отрезка. На каждом участке проверяем, являются ли его границы корнями, и есть ли на нём корень (принимает ли функция f разнознаковые значения на границах). В последнем случае используем “решатель” вроде nsolve (переданный по указателю), чтобы найти корень. Каждый найденный корень — это событие, вызываем для него “обработчик” — функцию обратного вызова по указателю report.
Функция qsort является частью Стандартной библиотеки C. Стандартная библиотека C++ предлагает более удобную и эффективную функцию sort (определённую в заголовочном файле ), однако её рассмотрение выходит за пределы темы данного раздела.
Следующий пример является развитием примера со списком из предыдущего подраздела и использует бестиповые указатели, указатели на указатели и указатели на функции для управления “обобщённым” связанным списком в стиле C. Звенья такого списка могут содержать произвольные данные. Основное требование к звеньям списка — наличие в начале звена указателя на следующее звено, фактически каждый предыдущий указатель указывает на следующий.
Теперь сама программа, выводящая строки в обратном порядке, упрощается:
Впрочем, необходимо отметить, что сочетая такие приёмы со средствами C++, выходящими за пределы “чистого” C, вы рискуете нарваться на неопределённое поведение. Низкоуровневые средства требуют особой внимательности, так как компилятор в таких случаях не страхует программиста. В частности, в общем случае нельзя интерпретировать произвольный указатель как void* и наоборот без выполнения приведения типа. А это может произойти неявно, например, в примере выше мы полагаем, что указатель prev, указывающий на объект структуры Line совпадает с указателем на поле prev этого объекта.
Синтаксическая справка
Правило чтения сложных описаний типов
Конструкции, определяющие переменные или вводящие новые типы в языках C и C++, могут порой иметь довольно запутанный вид. Ниже дано правило, помогающее разобраться в смысле сложных конструкций.
Некоторые примеры “расшифровки” типов переменных:
Разница между typedef и using
В С++11 появилась возможность объявлять синонимы типов с помощью using-директивы в стиле инициализации переменных:
Типы, ассоциируемые с массивами
Пусть N — константа времени компиляции и дано определение
Типы, ассоциируемые с функциями
Пусть дано объявление
Указатели на функции-члены и реализация самых быстрых делегатов на С++.
Автор: Don Clugston
CSG Solar Pty Ltd
Перевод: Денис Буличенко
Источник: Member Function Pointers and the Fastest Possible C++ Delegates
Материал предоставил: RSDN Magazine #6-2004
Опубликовано: 27.07.2005
Исправлено: 17.09.2005
Версия текста: 1.0
Введение
В этой статье я приоткрою завесу над указателями на функции-члены. После напоминания о синтаксисе и идеологии указателей на функции-члены, я объясню, как все это реализуется на наиболее популярных компиляторах. Покажу, как компиляторы могут эффективно реализовать делегатов. И наконец, я продемонстрирую, как, используя все эти знания, сделать реализацию оптимально эффективных делегатов. Например, вызов простого делегата на Visual C++ генерирует всего лишь две строчки ассемблерного кода!
Указатели на функции
Заметьте, что различным комбинациям аргументов соответствуют различные типы указателей на функцию. В MSVC, кроме этого, указатели на функцию различаются в зависимости от типа соглашения о вызове (calling conventions): __cdecl, __stdcall, __fastcall. Для того чтобы указатель на функции указывал на вашу функцию, необходимо выполнить следующую конструкцию:
Для вызова функции через указатель:
Наиболее распространенное применение указателей на функции в С – это использование библиотечных функций, таких как qsort, и обратных (callback) функций в Windows. Кроме того, есть еще много вариантов их применения. Реализация указателей на функции проста: это всего лишь «указатели на код», в них содержится начальный адрес участка ассемблерного кода. Различные типы указателей существуют лишь для уверенности в корректности применяемого соглашения о вызове.
Указатели на функции-члены
В программах на С++, большинство функций являются членами. Это означает, что они являются частью класса. И использовать обычный указатель на функцию в этом случае нельзя. Вместо этого нужно использовать указатель на функцию-член. Указатель на функцию-член класса SomeClass, с теми же аргументами, что и ранее, объявляется следующим образом:
Многие компиляторы (например, NSVC) разрешат вам пропустить взятие адреса (&), но более соответствующие стандарту (например, GNU G++) потребуют этого. Так что если пишете портируемый код, то не забывайте &. Для вызова метода через указатель на функцию-член нужно предоставить объект SomeClass и воспользоваться специальным оператором (->*). Данный оператор имеет низкий приоритет, так что его следует поместить в скобки:
Прошу меня не винить в ужасном синтаксисе – похоже, кто-то из разработчиков С++ любит знаки препинания!
Жуткие сведения об указателях на функции-члены
Рассмотрим ограничения, накладываемые на указатели на функции-члены. Во-первых, нельзя использовать указатель на функцию-член для статического метода. В этом случае нужно использовать обычный указатель на функцию (так что название «указатель на функцию-член» несколько некорректно, на самом деле это «указатель на нестатическую функцию-член»). Во-вторых, при работе с классами-наследниками есть несколько особенностей. Например, следующий код будет скомпилирован на MSVC, если оставить комментарии:
Довольно любопытно, &DerivedClass::some_member_func являтся указателем на функцию-член класса SomeClass. Это не член класса DerivedClass! Некоторые компиляторы ведут себя несколько иначе: например, Digital Mars C++ считает в данном случае, что &DerivedClass::some_member_func не определен. Но если DerivedClass переопределяет some_member_func, код не будет скомпилирован, т.к. &DerivedClass::some_member_func теперь становится указателем на функцию-член класса DerivedClass!
На некоторых компиляторах происходят ужасные вещи, даже при приведении между указателями на члены базового и наследуемого классов. При множественном наследовании использование 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) (вызов метода портируем, но не определен стандартом). В-третьих, разрешение приводить указатели на функции-члены без разрешения осуществлять последующий вызов совершенно неприменимо. Но если и приведение, и вызов разрешены, то легко реализовать эффективные делегаты с большой пользой для языка.
Чтобы убедить вас в этом противоречивом утверждении, рассмотрим файл, состоящий лишь из следующего кода. Это правильный код на С++.
Для объяснения второй части моего утверждения, что приведение работает не так, как указано в Стандарте, нужно в деталях рассмотреть, как компиляторы реализуют указатели на функции-члены. Также это поможет объяснить, почему правила использования указателей на функции-члены такие строгие. Трудно найти точную документацию по указателям на функции-члены, много неверной информации, и я исследовал ассемблерный код, генерируемый множеством компиляторов. Итак, пришло время испачкать руки.
Указатели на функции-члены – почему они такие сложные?
Функции-члены классов несколько отличаются от стандартных функций. Кроме обычных параметров они принимают скрытый параметр, называемый 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)).
Ни одной из этих сложностей не было бы, если бы в С++ указатели на функции-члены были определены несколько иначе. В приведенном выше коде сложность появляется из-за того, что разрешено ссылаться на A::Afunc как на D::Afunc. Вероятно, это плохой стиль. Обычно следует использовать базовые классы как интерфейсы. Если бы вы делали только так, указатели на функции члены были бы обычными указателями на функции со специальным соглашением о вызове. На мой взгляд, разрешение указывать на переопределенные функции было трагической ошибкой. Из-за этой редко используемой функциональности указатели на функции-члены стали нелепицей. Кроме того, они причиняют головную боль вынужденным реализовать их авторам компиляторов.
Реализация указателей на функции-члены
Итак, как же компиляторы реализуют указатели на функции-члены? В таблице приведены результаты применения оператора sizeof к различным структурам (int, указатель на данные void*, указатель на код (т.е. указатель на статическую функцию), и указатель на функцию-член класса с одиночным, множественным, виртуальным наследованиями, или неопределенного класса (т.е. объявленного позже)) для различных 32, 64 и 16 битных компиляторов.
Компилятор | Ключи | int | Указатель на данные | Указатель на код | Одиночное наследование | Множественное наследование | Виртуальное наследование | Неопределенное наследование |
---|---|---|---|---|---|---|---|---|
MSVC | 4 | 4 | 4 | 4 | 8 | 12 | 16 | |
MSVC | /vmg | 4 | 4 | 4 | 16# | 16# | 16# | 16 |
MSVC | /vmg/vmm | 4 | 4 | 4 | 8# | 8# | — | 8 |
Intel_IA32 | 4 | 4 | 4 | 4 | 8 | 12 | 16 | |
Intel_IA32 | /vmg/vmm | 4 | 4 | 4 | 4 | 8 | — | 8 |
Intel_Itanium | 4 | 8 | 8 | 8 | 12 | 16 | 20 | |
G++ | 4 | 4 | 4 | 8 | 8 | 8 | 8 | |
Comeau | 4 | 4 | 4 | 8 | 8 | 8 | 8 | |
DMC | 4 | 4 | 4 | 4 | 4 | 4 | 4 | |
BCC32 | 4 | 4 | 4 | 12 | 12 | 12 | 12 | |
BCC32 | /Vmd | 4 | 4 | 4 | 4 | 8 | 12 | 12 |
WCL386 | 4 | 4 | 4 | 12 | 12 | 12 | 12 | |
CodeWarrior | 4 | 4 | 4 | 12 | 12 | 12 | 12 | |
XLC | 4 | 8 | 8 | 20 | 20 | 20 | 20 | |
DMC | Small | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
Medium | 2 | 2 | 4 | 4 | 4 | 4 | 4 | |
WCL | Small | 2 | 2 | 2 | 6 | 6 | 6 | 6 |
Compact | 2 | 4 | 2 | 6 | 6 | 6 | 6 | |
Medium | 2 | 2 | 4 | 8 | 8 | 8 | 8 | |
Large | 2 | 4 | 4 | 8 | 8 | 8 | 8 |
# Или 4, 8 или 12 при использовании ключевого слова __single/__multi/ __virtual_inheritance
ПРИМЕЧАНИЕ При создании таблицы использованы следующие компиляторы: Microsoft Visual C++ от 4.0 до 7.1 (.NET 2003), GNU G++ 3.2 (бинарные файлы MingW, www.mingw.org ), Open Watcom (WCL) 1.2 ( www.openwatcom.org ), Digital Mars (DMC) 8.38n ( www.digitalmars.com ), Intel C++ 8.0 для Windows ( www.metrowerks.com ), Comeau C++ 4.3 ( www.comeaucomputing.com ). Данные по Comeau относятся ко всем поддерживаемым ими 32-битными платформам (x86, Alpha, SPARC и т.д.). Также были протестированы 16-битные компиляторы в четырех DOS-конфигурациях (tiny, compact, medium и large) для демонстрации влияния различного кода на размер указателей. MSVC был также протестирован с опцией (/vmg), которая дает «полную общность указателям на функции-члены» Удивительно, не правда ли? Взглянув на эту таблицу, вы сразу поймете, как просто написать код, который будет работать при некоторых условиях, и не будет компилироваться при других. Внутренняя реализация в разных компиляторах сильно различается. Не думаю, что такое разнообразие реализаций можно встретить у любого другого свойства языка. Детальный взгляд на реализации выдает некоторые удивительные грязные трюки. Как правило, компиляторы готовы к худшему, и всегда используют наиболее общую форму. Далее приведена структура, которую они используют: Почти во всех компиляторах два поля, которые я назвал delta и vindex, используются для подстройки указателя this для передачи в функцию. Например, вычисления Borland выглядят следующим образом: Borland применяет оптимизацию: если класс использует лишь одиночное наследование, то компилятор знает, что delta и vindex всегда равны 0, так что в большинстве случаев он может пропустить вычисления. Digital Mars C++ (ранее Zortech C++, затем Symantec C++ – между прочим, это был первый из С++-компиляторов, компилирующий в native-код) использует другой метод оптимизации. Для классов, использующих одиночное наследование, указатель на функцию-член – это всего лишь адрес функции. В случае более сложного наследования, указатель на функцию-член указывает на дополнительную функцию, которая вводит необходимые поправки в указатель this, после чего вызывает реальную функцию-член. Каждая из таких маленьких дополнительных функций создается для каждого метода, участвующего во множественном наследовании. Это наиболее эффективная реализация. Компилятор GNU использует странную и хитрую оптимизацию. Она основана на том, что при виртуальном наследовании нужно просматривать таблицу vtable для того, чтобы найти voffset, требуемый для вычисления указателя this. Поэтому в таблице можно хранить еще и указатель на функцию. Таким образом, они объединяют поля m_func_address и m_vtable_index в одно, и различают их, опираясь на то, что указатели на функции всегда указывают на четные адреса, а индексы таблицы vtable всегда нечетные. Выполняются следующие вычисления: Компиляторы, основанные на решениях от Edison Design Group (Comeau, Portland Group, Greenhills), экономят место, используя 16-битные поля, где это возможно. Они выполняют следующие вычисления (32-битные версии): Большинство компиляторов для встраиваемых систем не разрешают множественного наследования. Поэтому они избегают всех этих причуд: указатели на функции-члены – это обычные указатели на обычные функции со скрытым параметром this. Грязная история о технологии Microsoft «меньший для класса»Компиляторы от Microsoft используют оптимизацию, схожую с используемой компиляторами от Borland. Но, в отличие от Borland, по умолчанию убираются поля, которые всегда равны нулю. Это означает, что указатели на функции-члены в присутствии одиночного наследования имеют такой же размер, как и указатели на обычные функции, при множественном наследовании их размер увеличивается, при появлении виртуального наследования размер становится еще больше. Это сохраняет место в памяти. Но это не соответствует стандарту, и имеет некоторые ужасные побочные эффекты. Во-первых, приведение указателя на функцию-член класса-наследника и базового класса может изменить размер этого указателя! В частности, может быть потеряна информация. Во-вторых, при объявлении указателя на функцию-член до определения его класса, компилятор должен как-то определить размер указателя, который нужно выделить. Но компилятор не может точно определить нужный размер, т.к. он не знает природы наследования, используемой классом, до того, как класс определен. Компилятору остается только угадывать. Если он ошибется в одном исходном файле, но корректно угадает в другом случае, программа будет необъяснимо падать во время работы. Поэтому Microsoft добавил в свой компилятор несколько зарезервированных слов: __single_inheritance, __multiple_inheritance и __virtual_inheritance. Также был добавлен ключ компилятора /vmg, который делает размер всех указателей на функции-члены одинаковым, сохраняя нулевые поля. Теперь история становится подленькой. В документации сказано, что использование ключа /vmg эквивалентно объявлению каждого класса с использованием ключевого слова __virtual_inheritance. Однако это не так. Вместо этого компилятор использует еще более крупную структуру, которую я назову unknown_inheritance. Данная структура используется при создании указателя на функцию-член класса, описанного позже. Они не могут использовать __virtual_inheritance указатели, потому что используется глупая оптимизация. Вот используемый алгоритм: В случае виртуального наследования, значение vtordisp не содержится в __virtual_inheritance-указателе! Вместо этого компилятор жестко зашивает это значение в ассемблерный код. Но для работы с неопределенными типами это значение нужно знать. В итоге они пришли к двум типам указателей на функции-члены в присутствие виртуального наследования. Но до выхода VC7 случай unknown_inheritance был безнадежно глючен. Поля vtordisp и vindex были всегда равны нулю! Ужасающий вывод: на VC4 – VC6 определение опции /vmg (без /vmm или /vms) могло привести к вызову неправильной функции! И это было крайне трудно отследить. В VC4 было окошко для выбора опции /vmg, но оно было отключено. Я подозреваю, что кто-то в Microsoft знал об этой ошибке, но она никогда не была описана в их базе знаний. В конце концов они исправили ошибку в VC7. Intel использует те же вычисления, что и MSVC, но их опция /vmg ведет себя иначе (она влияет только на unknown_inheritance). А еще есть CodePlay. В их VectorC есть опция для совместимости с Microsoft VC6, GNU и Metrowerks при компоновке. Учитывая все вышесказанное, было бы очень впечатляюще, если бы они смогли обеспечить совместимость для указателей на функции-члены. К сожалению, это почти невозможно. Поэтому они всегда используют способ Microsoft. Я подозреваю, что они провели его реинжениринг, прямо как я. Но кажется, они не смогли не заметили случая unknown_inheritance и значения vtordisp. Их вычисления неявно (и некорректно) предполагают, что vtordisp=0, из-за чего в некоторых (неопределенных) случаях может быть вызвана не та функция. Так что их случай виртуального наследования не работает, и если написать код, создающий unknown_inheritance-указатель, будет выдано страшненькое сообщение об ошибке. And then there’s CodePlay. Codeplay’s VectorC has options for link compatibility with Microsoft VC6, GNU, and Metrowerks. Based on what we’ve seen, it would be very impressive if they could do this for member function pointers. Sadly, that’s nearly impossible. Instead, they always use the Microsoft method. I suspect they reverse-engineered it, just as I have. But it seems they didn’t detect the unknown_inheritance case, or the vtordisp value. Their calculations implicitly (and incorrectly) assume vtordisp=0, so the wrong function can be called in some (obscure) cases. So their virtual inheritance case doesn’t work, and if you write code that would create an unknown_inheritance pointer, you get a bizarre error message. Что из этого следует?Теоретически все эти производители могут радикально изменить технику работы с указателями на функции-члены. Фактически же, это крайне маловероятно, т.к. испортится много существующего кода. В MSDN есть очень старая статья, опубликованная Microsoft и объясняющая детали реализации времени исполнения в Visual C++ [JanGray]. Статья написана Яном Греем (Jan Gray), который писал объектную модель MS C++ в 1990 году. Несмотря на то, что статья датирована 1994 годом, она все так же актуальна – не считая исправленной ошибки, ничего за 15 лет так и не изменилось. Аналогично, мой первый компилятор (Borland C++ 3.0, (1990)) генерирует код, схожий с кодом современного компилятора от Borland, за тем исключением, что 16-битные регистры заменены 32-битными. ДелегатыВ отличие от указателей на функции-члены, нетрудно найти применение для делегатов. Они могут быть применены везде, где использовались указатели на функции в программах на С. Возможно, наиболее важно, что при помощи делегатов очень просто реализовать улучшенную версию паттерна Объект/Наблюдатель (Subject/Observer) [GoF]. Паттерн Наблюдатель наиболее применим в реализации графического интерфейса (GUI), но я заметил, что этот паттерн дает еще большие преимущества при использовании в основе приложения. Делегаты также позволяют элегантно реализовать паттерны Стратегия [GoF] и Состояние [GoF]. Однако нет способа сгенерировать такой эффективный код, используя лишь стандартный С++. Borland решает эту проблему добавлением в свой компилятор дополнительного ключевого слова (__closure), позволяя использовать понятный синтаксис и генерировать оптимальный код. Компилятор GNU также добавляет языковое расширение, но оно не совместимо с решением от Borland. При использовании любого из этих расширений вы ограничите себя одним компилятором. Если же вместо этого вы ограничитесь использованием стандартных средств С++, реализовать делегаты все-таки получится, они всего лишь не будут так эффективны. Мотивация: потребность в очень быстрых делегатахСуществует множество реализаций делегатов с использованием только стандартного С++. Все они используют одну и ту же идею. Основное наблюдение в том, что указатели на функции-члены работают, как и делегаты – но они работают лишь с одним классом. Чтобы обойти это ограничение, нужно добавить еще один уровень перенаправления: можно использовать шаблоны для создания «класса-вызывателя», вызывающего метод для каждого класса. Делегат будет содержать указатель this и указатель на «класс-вызыватель». Класс-вызыватель должен быть создан в куче. Существует много реализаций данной схемы, в том числе и на CodeProject. Они различаются по сложности, синтаксису (особенно близостью синтаксиса к С#) и универсальности. Окончательная реализация – это boost::function. Недавно она была внесена в Стандарт С++. [Sutter1] Скорее всего ее ждет широкое применение. Несмотря на гибкость стандартных реализаций, меня они не удовлетворяют. Несмотря на то, что они предоставляют требуемую функциональность, они пытаются скрыть свою основу: теряется низкоуровневая конструкция. Не стоит надеяться, что на всех платформах код «класса-вызывателя» будет одинаковым почти для всех классов. Но наиболее важно, то, что используется куча (heap). Для некоторых приложений это недопустимо. У меня есть проект – симулятор дискретных событий. Ядром такой программы является распределитель, диспетчер сообщений, который вызывает методы различных симулируемых объектов. Большинство таких методов очень просты: они всего лишь изменяют внутреннее состояние объекта и иногда добавляют новые события в очередь. Это прекрасная ситуация для использования делегатов. Однако каждый делегат вызывается лишь один раз. Изначально я использовал boost::function, но я заметил, что выделение памяти для делегатов занимало более трети от общего времени работы! Мне нужны были настоящие делегаты. Такие делегаты, чтобы состояли всего из двух ассемблерных инструкций! Хитрость: приведение любого указателя на функцию-член к стандартной формеОснова моего кода – класс, который конвертирует произвольный указатель на класс и произвольный указатель на функцию-член в общий указатель на класс и общую функцию-член. В С++ нет типа «общая функция-член», поэтому я привожу к функциям-членам неопределенного класса CGenericClass. Большинство компиляторов обращаются со всеми указателями на функции-члены одинаково, независимо от класса. Для большинства из них непосредственное reinterpret_cast<> приведение от конкретного указателя на функцию-член к общему указателю на функцию-член будет работать. На самом деле, если это не так, то компилятор не соответствует стандарту. Для оставшихся компиляторов (Microsoft Visual C++ и Intel C++) нам придется преобразовывать указатели на функции-члены классов с множественным или виртуальным наследованием к указателям на функции-члены класса с одиночным наследованием. Для этого нужна некоторая магия с шаблонами и ужасный хак. Обратите внимание – хак нужен только потому, что эти компиляторы не соответствуют стандарту, но наградой за этот хак будет оптимальный код. Зная внутреннее представление указателей на функции-члены в компиляторах и то, как нужно подправить указатель this при вызове функции, мы можем сами его изменить при создании делегата. Для указателей в условиях одиночного наследования никакой поправки this не требуется. При множественном наследовании требуется простое сложение. В случае виртуального наследования…. Ужас. Но это работает, и в большинстве случаев вся работа выполняется во время компиляции! И теперь последняя хитрость. Как различать различные типы наследования? Нет стандартного способа определить, использует класс множественное наследование или нет. Но есть хитрый способ, который можно найти в таблице 1, представленной выше – в MSVC каждый вид наследования различается по размеру указателя на метод класса! Итак, используем специализацию шаблонов на основе размера указателя на функцию-член! Для множественного наследования вычисления тривиальны. Схожее, но намного более сложное вычисление используется в случае unknown_inheritance (16 байт). Очень существенное дополнительное преимущество реализации делегатов с помощью такого нестандартного приведения состоит в том, что делегаты могут сравниваться на равенство. Большинство существующих реализаций не могут этого делать, это затрудняет их использование в некоторых задачах, таких как реализация multicast-делегатов [Sutter3]. Статические функции как цели делегатовВ идеале, простая функция (не функция-член), или статическая функция-член, может быть целью делегата. Этого можно достигнуть, преобразовав статическую функцию в функцию-член. Я могу предложить два способа конвертации, в обоих делегат указывает на «вызывающую» функцию-член, которая вызывает статическую функцию. Плохой метод использует хак. Вы можете хранить указатель на функцию вместо указателя this, так что при выполнении «вызывающей» функции нужно преобразовать this к указателю на статическую функцию. Приятная информация в том, что в коде нормальных функций-членов ничто не изменится. Проблема в том, что это хак, т.к. требуется приведение между указателем на данные и указателями на код. Это не будет работать на системах, где указатели на код больше, чем указатели на данные (DOS-компиляторы, использующие medium-модель памяти). Это будет работать на всех известных мне 32- и 64-битных компиляторах. Но т.к. это Зло, нужно найти альтернативное решение. Безопасный способ состоит в хранении указателя на функцию в отдельном параметре делегата. Делегат указывает на свою функцию-член. Всякий раз, когда делегат копируется, эти ссылки сами на себя должны быть преобразованы, это же относится к операторам = и ==. Это увеличит размер делегата на 4 байта, а также увеличивает сложность кода, но не влияет на скорость вызова. Я реализовал оба метода, т.к. оба имеют свои преимущества: безопасный метод гарантировано работает, плохой метод генерирует ассемблерный код, какой был бы сгенерирован при встроенной поддержке делегатов. По умолчанию мой код использует безопасный метод, другой же включается при помощи включения макроса: Использование кодаБыстрые делегаты работают с любой комбинацией параметров, но для того, чтобы они работали на всех возможных компиляторах, нужно определить количество параметров при объявлении делегатов. Максимум – восемь параметров, но увеличить этот предел тривиально. Используется область видимости fastdelegate. Вся грязная работа выполняется в области видимости detail. Вот выдержка из FastDelegateDemo.cpp, демонстрирующая большинство разрешенных операций. CBaseClass – виртуальный базовый класс для CDerivedClass. Пример только демонстрирует синтаксис. Возвращаемые значения, отличные от voidВ версии 1.3 в код добавлена возможность иметь возвращаемые значения, отличные от void. Как и в std::unary_function, возвращаемый тип – это последний параметр. По умолчанию это void, который используется для совместимости с прежними версиями. Это означает также, что наиболее распространенный случай остается простым. Мне хотелось добиться этого без потери в производительности на любой платформе. Как оказалось, это несложно сделать на всех компиляторах, кроме MSVC6. Два главных ограничения VC6: Эти ограничения можно обойти при помощи двух трюков: После этого потребуется изменение предыдущего кода: все экземпляры FastDelegate0 должны быть заменены на FastDelegate0<>. Это изменение можно произвести при помощи глобального Search-and-Replace. Мне кажется, это исправление приведет к более понятному синтаксису: объявление любого вида void FastDelegate теперь полностью похоже на объявление функции, единственное отличие в том, что () заменено <>. Если это изменение кого-то сильно раздражает, можно немного подправить заголовочный файл: обернуть объявление FastDelegate0<> в отдельную область видимости namespace newstyle <>, и определить синоним Также нужно изменить соответствующую функцию MakeDelegate. Использование FastDelegate как аргумента функции.Шаблон MakeDelegate позволяет использовать FastDelegate как простое замещение указателя на функцию. Обычное применение – использовать FastDelegate как закрытый член класса и модифицирующую функцию для его установки (как __event у Microsoft). Например: ЛицензияИсходный код, связанный с этой статьей выложен для всеобщего использования. Вы можете его использовать в любых целях. Если честно, написание статьи заняло в десять раз больше времени, чем написание кода. ПереносимостьПоскольку вся работа основана на поведении не определенном в стандарте, я аккуратно проверил код на многих компиляторах. Удивительно, но он намного более переносим, нежели огромное количество «стандартного» кода, т.к. большинство компиляторов не совсем соответствуют стандарту. Реализация FastDelegate протестирована на Windows, SPARC, DOS, Solaris, на некоторых Linux, использующих x86, AMD64, Itanium, SPARC и некоторые другие процессоры. Проверены следующие компиляторы: Кроме того, код ядра (приведение указателей на функции члены) был проверен на MSVC 1.5 и 4.0, Open Watcom WCL 1.2, но эти компиляторы не поддерживают шаблоны методов, так что на них не удалось скомпилировать полный исходный код. IBM утверждает о 100% бинарной совместимости VisualAge и XLC с GCC-компиляторами, так что они тоже должны работать. Если у вас есть компилятор, не приведенный здесь, и вы не против поработать бета-тестером, то дайте мне знать. ЗаключениеТо, что начиналось с объяснения нескольких строчек кода, написанных мною, превратилось в огромное учебное пособие по опасным аспектам слабоосвещенной части языка. Я также нашел ранее незарегистрированные ошибки и несовместимости в четырех популярных компиляторах. Это ужасное количество работы для двух строк ассемблерного кода. Я надеюсь, помог избавиться от некоторого недопонимания работы указателей на функции-члены и делегатов. Было показано, что причина большинства неприятностей с указателями на функции-члены – большие различия в реализации разных компиляторов. Также было показано, что, вопреки распространенному мнению, делегаты вовсе не сложная высокоуровневая конструкция, а очень простая. Я надеюсь, убедил вас, что они должны быть частью языка. Есть большая надежда, что в некоторой форме компиляторы напрямую будут поддерживать делегаты, с выходом нового стандарта. Насколько я знаю, другой столь же эффективной реализации делегатов, как представленная мной, нет. Надеюсь, код будет вам полезен.
|