Подавление подстановки кода функций объясняет один факт, который кажется невероятным многим опытным программистам C: функция C++
sort
почти всегда превосходит по скорости функцию C
qsort
. Конечно, в C++ приходится создавать экземпляры шаблонов функций и вызывать
operator
, тогда как в C все ограничивается простым вызовом функции, однако все «излишества» C++ теряются во время компиляции. На стадии выполнения
sort
обращается к подставленной функции сравнения (при условии, что функция была объявлена с ключевым словом
inline
, а ее тело доступно на стадии компиляции), тогда как
qsort
вызывает функцию сравнения через указатель. Результат —
sort
работает гораздо быстрее. В моих тестах с вектором, содержащим миллион чисел
double
, превосходство по скорости достигало 670%, но я не призываю верить мне на слово. Вы легко убедитесь в том, что при передаче объектов функций в качестве параметров алгоритмов «плата за абстракцию» превращается в «премию за абстракцию».
Существует и другая причина для передачи объектов функций в параметрах алгоритмов, не имеющая ничего общего с эффективностью. Речь идет о компилируемости программ. По каким-то загадочным причинам некоторые платформы STL отвергают абсолютно нормальный код — это связано с недоработками то ли компилятора, то ли библиотеки, то ли и того и другого. Например, одна распространенная платформа STL отвергает следующий (вполне допустимый) фрагмент, выводящий в
cout
длину всех строк в множестве:
set<string> s;
…
transform(s.begin, s.end,
ostream_iterator<string::size_type>(cout, "\n"),
mem_fun_ref(&string::size)
);
Проблема возникает из-за ошибки в работе с константными функциями классов (такими как
string::size
) в этой конкретной платформе STL. Обходное решение заключается в использовании объекта функции:
struct StringSize:
public_unary_function<string, string::size_type> { // См. совет 40
Существуют и другие обходные решения, но приведенный фрагмент хорош не только тем, что он компилируется на всех известных мне платформах STL. Он также делает возможной подстановку вызова
string::size
, что почти наверняка невозможно в предыдущем фрагменте с передачей
mem_fun_ref(&string::size)
. Иначе говоря, определение класса функтора
StringSize
не только обходит недоработки компилятора, но и может улучшить быстродействие программы.
Другая причина, по которой объекты функций предпочтительнее обычных функций, заключается в том, что они помогают обойти хитрые синтаксические ловушки. Иногда исходный текст, выглядящий вполне разумно, отвергается компилятором по законным, хотя и неочевидным причинам. Например, в некоторых ситуациях имя экземпляра, созданного на базе шаблона функции, не эквивалентно имени функции. Пример:
template<typename FPType> // Вычисление среднего
FPType average(FPType val1, FPType val2) // арифметического двух
Многие компиляторы принимают этот код, но по Стандарту C++ он считается недопустимым. Дело в том, что теоретически может существовать другой шаблон функции с именем
average
, вызываемый с одним параметром-типом. В этом случае выражение
становится неоднозначным, поскольку непонятно, какой шаблон в нем упоминается. В конкретном примере неоднозначность отсутствует, но некоторые компиляторы на вполне законном основании все равно отвергают этот код. Решение основано на использовании объекта функции:
template<typename FPType>
struct Average:
public binary_function<FPType, FPType, FPType> { // См. совет 40
Новая версия должна приниматься любым компилятором. Более того, вызовы
Average::operator
внутри
transform
допускают подстановку кода, что не относится к экземплярам приведенного выше шаблона
average
, поскольку
average
является шаблоном функции, а не объекта функции.
Таким образом, преимущество объектов функций в роли параметров алгоритмов не сводится к простому повышению эффективности. Объекты функций также обладают большей надежностью при компиляции кода. Бесспорно, «настоящие» функции очень важны, но в области эффективного программирования в STL объекты функций часто оказываются полезнее.
Совет 47. Избегайте «нечитаемого» кода
Допустим, имеется вектор
vector<int>
. Из этого вектора требуется удалить все элементы, значение которых меньше х, но оставить элементы, предшествующие последнему вхождению значения, не меньшего у. В голову мгновенно приходит следующее решение: