Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс
Шрифт:
Интервал:
Закладка:
В библиотеке TR1 (см. правило 54) есть целый ряд новых классов-характеристик, которые предоставляют информацию о типах, включая is_fundamental<T> (где T – встроенный тип), is_array<T> (где T – тип массива) и is_base_of<T1,T2> (то есть является ли T1 тем же, что и T2, либо его базовым классом). Всего TR1 добавляет к стандартному C++ более 50 классов-характеристик.
Что следует помнить• Классы-характеристики делают доступной информацию о типах во время компиляции. Они реализованы с применением шаблонов и их специализаций.
• В сочетании с перегрузкой классы-характеристики позволяют проверять типы во время компиляции.
Правило 48: Изучите метапрограммирование шаблонов
Метапрограммирование шаблонов (template metaprogramming – TMP) – это процесс написания основанных на шаблонах программ на C++, исполняемых во время компиляции. На минуту задумайтесь об этом: шаблонная метапрограмма – это программа, написанная на C++, которая исполняется внутри компилятора C+ +. Когда TMP-программа завершает исполнение, ее результат – фрагменты кода на C++, конкретизированные из шаблонов, – компилируется как обычно.
Если эта идея не поразила вас до глубины души, значит, вы недостаточно напряженно думали о ней.
C++ не предназначался для метапрограммирования шаблонов, но с тех пор, как технология TMP была открыта в начале 90-х годов, она оказалась настолько полезной, что, вероятно, и в сам язык, и в стандартную библиотеку будут включены расширения, облегчающие работу с TMP. Да, TMP было именно открыто, а не придумано. Средства, лежащие в основе TMP, появились в C++ вместе с шаблонами. Нужно было только, чтобы кто-то заметил, как они могут быть использованы изобретательным и неожиданным образом.
Технология TMP дает два преимущества. Во-первых, она позволяет делать такие вещи, которые иными способами сделать было бы трудно либо вообще невозможно. Во-вторых, поскольку шаблонные метапрограммы исполняются во время компиляции C++, они могут переместить часть работы со стадии исполнения на стадию компиляции. В частности, некоторые ошибки, которые обычно всплывают во время исполнения, можно было бы обнаружить при компиляции. Другое преимущество – это то, что программы C++, написанные с использованием TMP, можно сделать эффективными почти во всех смыслах: компактность исполняемого, код быстродействия, потребления памяти. Но коль скоро часть работы переносится на стадию компиляции, то, очевидно, компиляция займет больше времени. Для компиляции программ, в которых применяется технология TMP, может потребоваться намного больше времени, чем для компиляции аналогичных программ, написанных без применения TMP.
Рассмотрим псевдокод шаблонной функции advance, представленный на стр. 227 (см. правило 47; возможно, имеет смысл перечитать это правило сейчас, поскольку ниже я предполагаю, что вы знакомы с изложенным в нем материалом). Я выделил в этом фрагменте часть, написанную на псевдокоде:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (iter является итератором с произвольным доступом) {
iter += d; // использовать итераторную арифметику
} // для итераторов с произвольным доступом
else {
if(d>=0) {while (d–) ++iter;} // вызывать ++ или – в цикле
else {while(d++) –iter;} // для итераторов других категорий
}
}
Мы можем использовать typeid, чтобы заменить псевдокод реальным кодом. Тогда задача будет решена «нормальным» для C++ способом – вся работа выполняется во время исполнения:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterT>::iterator_category)==
typeid(std::random_access_iterator_tag))
iter += d; // использовать итеративную арифметику
} // для итераторов с произвольным доступом
else {
if(d>=0) {while (d–) ++iter;} // вызывать ++ или – в цикле
else {while(d++) –iter;} // для итераторов других категорий
}
}
В правиле 47 отмечено, что подход, основанный на typeid, менее эффективен, чем при использовании классов-характеристик, поскольку в этом случае: (1) проверка типа происходит во время исполнения, а не во время компиляции, и (2) код, выполняющий проверку типа, должен быть включен в исполняемую программу. Фактически этот пример показывает, как технология TMP может порождать более эффективные программы на C++, потому что характеристики – это и есть частный случай TMP. Напомню, что характеристики делают возможным вычисление выражения if…else во время компиляции.
Я уже отмечал выше, что некоторые вещи технология TMP позволяет сделать проще, чем «нормальный» C++, и advance можно считать иллюстраций этого утверждения. В правиле 47 упоминается о том, что основанная на typeid реализация advance может привести к проблемам во время компиляции, и вот вам пример такой ситуации:
std::list<int>::iterator iter;
...
advance(iter, 10); // сдвинуть iter на 10 элементов вперед
// не скомпилируется для приведенной
// выше реализации
Рассмотрим версию advance, которая будет сгенерирована для этого вызова. После подстановки типов iter и 10 в качестве параметров шаблона IterT и DistT мы получим следующее:
void advance(std::list<int>::iterator& iter, int d)
{
if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category)==
typeid(std::random_access_iterator_tag))
iter += d; // ошибка!
}
else {
if(d>=0) {while (d–) ++iter;}
else {while(d++) –iter;}
}
}
Проблема в выделенной строке, где встречается оператор +=. В данном случае мы пытаемся использовать += для типа list<int>::iterator, но list<int>::iterator – это двунаправленный итератор (см. правило 47), поэтому он не поддерживает +=. Оператор += поддерживают только итераторы с произвольным доступом. Мы знаем, что никогда не попытаемся исполнить предложение, содержащее +=, потому что для list<int>::iterator проверка с привлечением typeid никогда не выполнится успешно, но компилятор-то обязан гарантировать, что весь исходный код корректен, даже если он никогда не исполняется, а «iter += d» – некорректный код в случае, когда iter не является итератором с произвольным доступом. Решение же на основе технологии TMP предполагает, что код для разных типов вынесен в разные функции, каждая из которых использует только операции, применимые к типам, для которых она написана.
Было доказано, что технология TMP представляет собой полную машину Тьюринга, то есть обладает достаточной мощью для любых вычислений. Используя TMP, вы можете объявлять переменные, выполнять циклы, писать и вызывать функции и т. д. Но такие конструкции выглядят совершенно иначе, чем их аналоги из «нормального» C++. Например, в правиле 47 показано, как в TMP условные предложения if…else выражаются с помощью шаблонов и их специализаций. Но такие конструкции можно назвать «TMP уровня ассемблера». В библиотеках для работы с TMP (например, MPL из Boost – см. правило 55) предлагается более высокоуровневый синтаксис, хотя его также нельзя принять за «нормальный» С++.
Чтобы взглянуть на TMP с другого боку, посмотрим, как там выглядят циклы. Технология TMP не предоставляет настоящих циклических конструкций, поэтому цикл моделируется с помощью рекурсии. (Если вы не очень уверенно владеете рекурсией, придется освоиться с ней прежде, чем приступать к использованию TMP. Ведь TMP – по существу функциональный язык, а для таких языков рекурсия – то же, что телевидение для американской поп-культуры – неотъемлемая принадлежность.) Но и рекурсия-то не совсем обычная, поскольку при реализации циклов TMP нет рекурсивных вызовов функций, а есть рекурсивные конкретизации шаблонов.
Аналогом программы «Hello World» на TMP является вычисление факториала во время компиляции. Конечно, она, как и «Hello World», не поразит воображение, но обе полезны для демонстрации базовых возможностей языка. Вычисление факториала с помощью TMP сводится к последовательности рекурсивных конкретизаций шаблона. Кроме того, демонстрируется один из способов создания и использования переменных в TMP. Смотрите:
template<unsigned n> // общий случай: значение Factorial<n> – это
struct Factorial { // произведение n и Factorial<n-1>
enum { value = n*Factorial<n-1>::value };
};
template<> // частный случай: значение Factorial<0> –
struct Factorial<0> { // это 1
enum { value = 1 };
};
Имея такую шаблонную метапрограмму (на самом деле просто единственную шаблонную метафункцию Factorial), вы получаете значение факториала n, обращаясь к Factorial<n>::value.
Циклическая часть кода возникает там, где конкретизация шаблона Factorial<n> ссылается на конкретизацию шаблона Factorial<n-1>. Как во всякой рекурсивной программе, здесь есть особый случай, прекращающий рекурсию. В данном случае это специализация шаблона Factorial<0>.