MyTetra Share
Делитесь знаниями!
О, смотри-ка какое хорошее место. Дайте два!
Правило 2: Предпочитайте const, enum и inline использованию #define
29.03.2014
20:15
Текстовые метки: C++, const, enum, inline, define
Раздел: Компьютер - Программирование - Язык C++ - Главы книги "Эффективное использование C++"

Это правило лучше было бы назвать «Компилятор предпочтительнее препроцессора», поскольку #define зачастую вообще не относят к языку C++. В этом и заключается проблема. Рассмотрим простой пример; попробуйте написать что-нибудь вроде:


#define ASPECT_RATIO 1.653


Символическое имя ASPECT_RATIO может так и остаться неизвестным компилятору или быть удалено препроцессором до того, как код поступит на обработку компилятору. Если это произойдет, то имя ASPECT_RATIO не попадет в таблицу символов. Поэтому в ходе компиляции вы получите ошибку (в сообщении о ней будет упомянуто значение 1.653, а не ASPECT_RATIO). Это вызовет путаницу. Если имя ASPECT_RATIO было определено в заголовочном файле, который писали не вы, то вы вообще не будете знать, откуда взялось значение 1.653, и на поиски ответа потратите много времени. Та же проблема может возникнуть и при отладке, поскольку выбранное вами имя будет отсутствовать в таблице символов.

Решение состоит в замене макроса константой:


const double AspectRatio = 1.653; // имена, записанные большими буквами,

// обычно применяются для макросов,

// поэтому мы решили его изменить


Будучи языковой константой, AspectRatio видима компилятору и, естественно, помещается в таблицу символов. К тому же в случае использования константы с плавающей точкой (как в этом примере) генерируется более компактный код, чем при использовании #define. Дело в том, что препроцессор, слепо подставляя вместо макроса ASPECT_RATIO величину 1.653, создает множество копий 1.653 в объектном коде, в то время как использование константы никогда не породит более одной копии этого значения.

При замене #define константами нужно помнить о двух особых случаях. Первый касается константных указателей. Поскольку определения констант обычно помещаются в заголовочные файлы (где к ним получает доступ множество различных исходных файлов), важно, чтобы сам указатель был объявлен с ключевым словом const, в дополнение к объявлению const того, на что он указывает. Например, чтобы объявить в заголовочном файле константную строку типа char*, слово const нужно написать дважды:


const char * const authorName = “Scott Meyers”;


Более подробно о сущности и применений слова const, особенно в связке с указателями, см. в правиле 3. Но уже сейчас стоит напомнить, что объекты типа string обычно предпочтительнее своих прародителей – строк типа char *, поэтому authorName лучше определить так:


const std::string authorName(“Scott Meyers”);


Второе замечание касается констант, объявляемых в составе класса. Чтобы ограничить область действия константы классом, необходимо сделать ее членом класса, и чтобы гарантировать, что существует только одна копия константы, требуется сделать ее статическим членом:


class GamePlayer {

private:

static const int NumTurns = 5; // объявление константы

int scores[NumTurns]; // использование константы

...

};


То, что вы видите выше, – это объявление NumTurns, а не ее определение. Обычно C++ требует, чтобы вы представляли определение для всего, что используете, но объявленные в классе константы, которые являются статическими и имеют встроенный тип (то есть целые, символьные, булевские) – это исключение из правил. До тех пор пока вы не пытаетесь получить адрес такой константы, можете объявлять и использовать ее без предоставления определения. Если же вам нужно получить адрес либо если ваш компилятор настаивает на наличии определения, то можете написать что-то подобное:


const int GamePlayer::NumTurns; // определение NumTurns; см. ниже,

// почему не указывается значение


Поместите этот код в файл реализации, а не в заголовочный файл. Поскольку начальное значение константы класса представлено там, где она объявлена (то есть NumTurns инициализировано значением 5 при объявлении), то в точке определения задавать начальное значение не требуется.

Отметим, кстати, что нет возможности объявить в классе константу посредством #define, потому что #define не учитывает области действия. Как только макрос определен, он остается в силе для всей оставшейся части компилируемого кода (если только где-то ниже не встретится #undef). Это значит, что директива #define неприменима не только для объявления констант в классе, но вообще не может быть использована для обеспечения какой бы то ни было инкапсуляции, то есть придать смысл выражению «private #define» невозможно. В то же время константные данные-члены могут быть инкапсулированы, примером может служить NumTurns.

Старые компиляторы могут не поддерживать показанный выше синтаксис, так как в более ранних версиях языка было запрещено задавать значения статических членов класса во время объявления. Более того, инициализация в классе допускалась только для целых типов и для констант. Если вышеприведенный синтаксис не работает, то начальное значение следует задавать в определении:


class CostEstimate {

private:

static const double FudgeFactor; // объявление статической константы

... // класса – помещается в файл заголовка

};

const double // определение статической константы

CostEstimate::FudgeFactor = 1.35; // класса – помещается в файл реализации


Обычно ничего больше и не требуется. Единственное исключение обнаруживается тогда, когда для компиляции класса необходима константа. Например, при объявлении массива GamePlayer::scores компилятору нужно знать размер массива. Чтобы работать с компилятором, ошибочно запрещающим инициализировать статические целые константы внутри класса, можно воспользоваться способом, известным под названием «трюка с перечислением». Он основан на том, что переменные перечисляемого типа можно использовать там, где ожидаются значения типа int, поэтому GamePlayer можно определить так:


class GamePlayer {

private:

enum { NumTurns = 5, NumExp = 10 }; /

int scores[NumTurns]; // По-сути, это определение массива с размером 5 через константу NumTurns

...

};


Этот прием стоит знать по нескольким причинам. Во-первых, поведение «трюка с перечислением» в некоторых отношениях более похоже на #define, чем на константу, а иногда это как раз то, что нужно. Например, можно получить адрес константы, но нельзя получить адрес перечисления, как нельзя получить и адрес #define. Если вы хотите запретить получать адрес или ссылку на какую-нибудь целую константу, то применение enum – хороший способ наложить такое ограничение. (Подробнее о поддержке проектных ограничений с помощью приемов кодирования можно узнать из правила 18). К тому же, хотя хорошие компиляторы не выделяют память для константных объектов целых типов (если только вы не создаете указателя или ссылки на объект), менее изощренные могут так поступать, а вам это, возможно, ни к чему. Как и #define, перечисления никогда не станут причиной подобного нежелательного распределения памяти.

Вторая причина знать о «трюке с перечислением» чисто прагматическая. Он используется в очень многих программах, поэтому нужно уметь распознавать этот трюк, когда вы с ним сталкиваетесь. Вообще говоря, этот прием – фундаментальная техника, применяемая при метапрограммировании шаблонов (см. правило 48).

Вернемся к препроцессору. Другой частый случай неправильного использования директивы #define – создание макросов, которые выглядят как функции, но не обременены накладными расходов, связанными с вызовом функций. Ниже представлен макрос, который вызывает некоторую функцию f c аргументом, равным максимальному из двух значений:


// вызвать f, передав ей максимум из a и b

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))


В этой строчке содержится так много недостатков, что даже не совсем понятно, с какого начать.

Всякий раз при написании подобного макроса вы должны помнить о том, что все аргументы следует заключать в скобки. В противном случае вы рискуете столкнуться с проблемой, когда кто-нибудь вызовет его с выражением в качестве аргумента. Но даже если вы сделаете все правильно, посмотрите, какие странные вещи могут произойти:


int a = 5, b = 0;

CALL_WITH_MAX(++a, b); // a увеличивается дважды

CALL_WITH_MAX(++a, b+10); // a увеличивается один раз


Происходящее внутри max зависит от того, с чем она сравнивается!

К счастью, вы нет нужды мириться с поведением, так сильно противоречащим привычной логике. Существует метод, позволяющий добиться такой же эффективности, как при использовании препроцессора. Но при этом обеспечивается как предсказуемость поведения, так и контроль типов аргументов (что характерно для обычных функций). Этот результат достигается применением шаблона встроенной (inline) функции (см. правило 30):


template <typename T>

inline void callWithMax(const T& a, const T& b) // Поскольку мы не знаем,

{ // что есть T, то передаем

f(a > b ? a : b); // его по ссылке на const -

} // см. параграф 20


Этот шаблон генерирует целое семейство функций, каждая из которых принимает два аргумента одного и того же типа и вызывает f с наибольшим из них. Нет необходимости заключать параметры в скобки внутри тела функции, не нужно заботиться о многократном вычислении параметров и т. д. Более того, поскольку callWithMax – настоящая функция, на нее распространяются правила областей действия и контроля доступа. Например, можно говорить о встроенной функции, являющейся закрытым членом класса. Описать нечто подобное с помощью макроса невозможно.

Наличие const, enum и inline резко снижает потребность в препроцессоре (особенно это относится к #define), но не устраняет ее полностью. Директива #include остается существенной, а #ifdef/#ifndef продолжают играть важную роль в управлении компиляцией. Пока еще не время отказываться от препроцессора, но определенно стоит задуматься, как избавиться от него в дальнейшем.

Что следует помнить

• Для простых констант директиве #define следует предпочесть константные объекты и перечисления (enum).

• Вместо имитирующих функции макросов, определенных через #define, лучше применять встроенные функции.


← Содержание ...
 
MyTetra Share v.0.35
Яндекс индекс цитирования