Д. Вандевурд, Н.М. Джосаттис - Шаблоны C++. Справочник разработчика (2003) (1160769), страница 79
Текст из файла (страница 79)
Продемонстрируем это на примере. фпад б1(1пг сопев й г) ( гесигп ++ (1пга) г; // Нерационально, но законно ) 1пс б2(1пг сопит а г) ( гегигп г; 1пс бЗ () ( гесигп 42; ) 1пг Йоо() ( 1пг рагим = О; 1пс апзмег = О; апзыег = б1(рагащ)з б2(рагащ); НЗ (); гесигп апзыег + рагат; Функция б1 ( ) содержит аргумент типа сопзг 1пс, который передается по ссылке. Обычно это означает, что объект, который передается в функцию, не модифицируется в ней.
Однако, если значение передаваемой величины поддается изменению, в программе на С++ молификатор сопит может быть законным путем отменен, а значение величины — модифицировано. (Можно возразить, что так делать неразумно, однако стандарт 440 Глава 22. Объекты-функции и обратные вызовы С++ допускает подобные операции.) Именно это и происходит в функции б1 ( ) . В силу вышесказанного компилятор, оптимизирующий сгенерированный код в пределах каждой функции (по такому принципу работает большинство компиляторов), должен учитывать, что в любой функции, в которую передаются ссылки или указатели иа объект, этот объект может быть изменен. При этом нужно учесть, что в общем случае компилятору доступна информация только об обьявлеиии функции, поскольку ее определеиие (или реализаг(ил) часто находится в другой единице трансляции.
Таким образом, в приведенном выше примере большинство компиляторов предполагают, что в функции й2() переменная рагаш тоже может быть модифицирована (иесмотря иа то, что иа самом деле это ие так). Фактически компилятор даже ие может предполагать, что функция ЕЗ ( ) ие модифицирует локальную переменную рагага Действительно, ведь у функций й1() и б2 () во время их вызова была возможность сохранить адрес этой переменной в глобально доступном указателе. Поэтому с точки зрения компилятора обладающего неполными сведениями, нет ничего невозможного в том, что в функции' йЗ () значение переменной рагагв будет изменено с помощью этого глобального указателя.
В результате обычные вызовы функций усложняют работу большинства компиляторов, заставляя их сохранять промежуточные значения многих переменных ие в регистрах процессора, а в основной памяти, доступ к которой осуществляется несколько дольше. Это мешает выполнить многие виды оптимизаций, для которых бы понадобилось перемещение машинного кода (вызов функции часто создает барьер такому перемещению). Существуют компиляторы С++, способные отследить многие подобные случаи использования псевдонимов (в области видимости функции б1 ( ) выражение г — это псевдоним объекта, который в области видимости функции 1оо () носит имя рагаш).
Однако зто достигается за счет уменьшения скорости компиляции, увеличения расхода ресурсов и снижения степени надежности полученного кода. Проекты, иа компиляцию которых с помощью обычных компиляторов потребовалось бы несколько минут, в таких системах компилируются несколько часов, а иногда и дней (при условии, что в распоряжении компилятора имеется несколько гигабайт памяти). Более того, компиляторы, о которых идет речь, как правило, намного сложнее, чем обычные, поэтому вероятность генерации неправильного кода в иих выше.
Даже если компилятор с повышенной степенью оптимизации сгеиерирует правильный код, в исходном коде могут содержаться непреднамеренные нарушения запутанных правил использования псевдонимов в языках программирования С и С++ . При г обычной оптимизации такие нарушения могут оказаться вполне безобидными, ио при улучшенной оптимизации оии превращаются в ошибки. Однако значительную помощь обычным оптимизаторам может оказать процесс встраивания функций. Предположим, что функции й1 (), 22 () и ЕЗ () обьявлены как встраиваемые. В этом случае компилятор преобразует код функции боо () таким образом, что ои будет эквивалентен приведенному ниже. г В качестве примера такой ошибки можно привести попытку доступа к переменной типа ппвгчпед 1пе с помощью обычного указателя нв ьпе (со знаком).
22.2. Указатели и ссылки на функции з.пс Йоо' () ( 1пс рагагп = Ор зпг апвыег = О; апвыег = ++(зппй)ракет; геТпгп апвьгег + рагаш; Даже обычный оптимизатор способен преобразовать этот код в следующий: з,пс йоо' ' () ( гесигп 2; Из этого примера видно, что преимущество использования встраиваемых функций не только в том, что они помогают избежать выполнения машинного кода, в котором вызывается последовательность функций, но и в том (и зачастую это важнее), что благодаря им оптимизатор получает информацию о процессах, происходящих с переменными, которые передаются в функции.
Причем же тут шаблоны? Скоро вы убедитесь, что с их помошью иногда можно организовать обратные вызовы, в процессе компиляции которых генерируется код, включающий в себя прямые, а иногда даже встраиваемые вызовы, в то время как обычные обратные вызовы привели бы к использованию косвенных вызовов. При этом можно достичь существенного повышения производительности работы полученного исполняемого файла 22.2.
Указатели и ссылки на функции Рассмотрим приведенное ниже довольно простое определение функции йоп ( ) . 'ехкегп "С++" чозс) йоо() с)тгоы() ( ) Можно было бы охарактеризовать тип этой функции как "функция без аргументов со связыванием С++, которая не возвращает значений и не генерирует исключений". По историческим причинам в формальном определении функции в языке С++ спецификация исключения не входит в описание типа функции . Однако в будущем ситуация может изз мениться. В любом случае разумно при создании новых функций указывать их спецификации исключений. Связывание имен (обычно "С" и "С++") представляет собой, по сути, часть типа, но во многих реализациях языка С++ это условие соблюдается недостаточно строго.
В частности, в таких системах вполне допустимо указателю на функцию со з Исторические причины этого ие совсем понятны, и в данной области стандарт С++ несколько непоследователен. Глава 22. Объекты-функции и обратные вызовы 442 связыванием С присваивать значение указателя на функцию со связыванием С++ и наоборот.
Это следствие того, что в большинстве платформ соглашения о вызовах, применимые для функций С и С++, идентичны, если эти функции обладают одинаковым подмножеством параметров и возвращают значения одних и тех же типов. В большинстве ситуаций выражение йоо неявным образом преобразуется в указатель на функцию йоо(). Заметим, что сам идентификатор йоо не является обозначением указателя, точно так же, как выражение йа после объявления 1пг Та[10); не обозначает указатель на массив (или на его первый элемент). Такое неявное преобразование функции (или массива) в указатель часто называют сведением (десау). Чтобы проиллюстрировать это преобразование, рассмотрим завершенную программу на С++, которая приведена ниже.
// Ецпсгогв/йипсрсг.срр $1пс1цде <1озггеат> $1пс1ис)е <суре1п/ро> уойг) Еоо() ( зМ: ."соцс « "Еоо() са11ед" «зсс)::епд1; ) сурейей уо1с) РооТ()/ // РооТ вЂ” это тип функции, // совпадающий с типом функции йоо() йпг пайп() ( Йоо() р // Прямой вызов // Вывод типов йоо и РооТ згс)::соус « "Турез об йоо: " « СуреЫ[йоо].паве() « '~п'; зМ::сонг « "Турез ой РооТ: " « Гуре16(РооТ].пал1е() « '1п'; РооТ* рб = йоо/ // Неявное преобразование (сведение); И() ' // косвенный вызов с помощью указателя; (*рй)(); // эквивалент вызова рй(] // Вывод типа указателя рй зсг)::соис « "турез ой рй: " « суреЫ(рй).пате() « '1п'; Роста гб = йоса // Веэ неявного преобразования; 22.2.
Указатели и ссылки на функции // косвенный вызов по ссылке кй (); // Вывод типа гй вас)::ссис « "Турев об кб: " « пуре16(гй).паве() « '~п'; Эта функция демонстрирует разнообразные варианты использования типов функций, включая некоторые не совсем необычные. В рассмотренном примере используется оператор суреЫ, возвращающий статический тип вес):: суре 3 пйо, функция паве () которого выводит типы некоторых выражений (см. раздел 5.6, стр. 79). Когда оператор суреэ'.6 применяется к типу функции, сведения типа не происходит.
Ниже приведен вывод рассматриваемой демонстрационной программы, скомпилированной с помощью одного из компиляторов С++. йоо () Турев Туре в йоо () Тоо () Турев бсо () Турев са12ес( о~ й: чозс(() об йооТ: чозс)() са11ес) са11ес1 ой рй: РооТ * са11ес) об кб: чоЫ() Как видите, в этом компиляторе определения типов имен сохраняются в том виде, в котором они возвращаются функцией паве () (например, вместо расширенной формы типа чойс( (*) () выводится тип РооТ *), однако зто не является требованием языка и в других компиляторах может не выполняться. В этом примере также показано, что ссылки на функции существуют как концепция языка, но вместо них почти всегда используются указатели на функции (чтобы избежать неоднозначности, по-видимому, лучше всего продолжать использовать именно их).
Обратите внимание, что на самом деле выражение боо — это так называемое 1-значение (1ча1ие), поскольку его можно связать со ссылкой на неконстантный тип. Однако модифицировать данное 1ча1ие невозможно. Заметим, что имя указателя нв функцию (например, рб) или имя ссылки на функцию (например, кТ) можно использовать точно так же, как и имя самой функции. Таким образом, указовель на функцию — это функтор, т.е. такой обьект, который при вызове функции может быть использован вместо ее имени. С другой стороны, поскольку ссылка — это не объект, ссылка на функцию функтором не является. Если вспомнить описанные ранее особенности прямых и косвенных вызовов, то станет понятно, что использование этих идентичных обозначений может привести к значительным различиям в производительности программы.
Глава 22. Объекты-функции и обратные вызовы 22.3. Указатели на функции-члены Чтобы понять, чем различаются указатели на обычные функции и указатели на функции-члены, полезно разобраться, как происходит вызов функции-члена в типичной реализации С++.