поддержке проектных ограничений с помощью приемов кодирования можно узнать из правила 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, лучше применять встроенные функции.
Правило 3: Везде, где только можно используйте const
Замечательное свойство модификатора const состоит в том, что он накладывает определенное семантическое ограничение: данный объект не должен модифицироваться, – и компилятор будет проводить это ограничение в жизнь. const позволяет указать компилятору и программистам, что определенная величина должна оставаться неизменной. Во всех подобных случаях вы должны обозначить это явным образом, призывая себе на помощь компилятор и гарантируя тем самым, что ограничение не будет нарушено.
Ключевое слово const удивительно многосторонне. Вне классов вы можете использовать его для определения констант в глобальной области или в пространстве имен (см. правило 2), а также для статических объектов (внутри файла, функции или блока). Внутри классов допустимо применять его как для статических, так и для нестатических данных-членов. Для указателей можно специфицировать, должен ли быть константным сам указатель, данные, на которые он указывает, либо и то, и другое (или ни то, ни другое):
char greeting[] = “Hello”;
char *p = greeting; // неконстантный указатель,
// неконстантные данные
const char *p = greeting; // неконстантный указатель,
// константные данные
char * const p = greeting; // константный указатель,
// неконстантные данные
const char * const p = greeting; // константный указатель,
// константные данные
Этот синтаксис не так страшен, как может показаться. Если слово const появляется слева от звездочки, константным является то, на что указывает указатель; если справа, то сам указатель является константным. Наконец, если же слово const появляется с обеих сторон, то константно и то, и другое.
Когда то, на что указывается, – константа, некоторые программисты ставят const перед идентификатором типа. Другие – после идентификатора типа, но перед звездочкой. Семантической разницы здесь нет, поэтому следующие функции принимают параметр одного и того же типа:
void f1(const Widget *pw); // f1 принимает указатель на
// константный объект Widget
void f1(Widget const *pw); // то же самое делает f2
Поскольку в реальном коде встречаются обе формы, следует привыкать и к той, и к другой.
Итераторы STL смоделированы на основе указателей, поэтому iterator ведет себя почти как указатель T*. Объявление const-итератора подобно объявлению const-указателя (то есть записи T* const): итератор не может начать указывать на что-то другое, но то, на что он указывает, может быть модифицировано. Если вы хотите иметь итератор, который указывал бы на нечто, что запрещено модифицировать (то есть STL- аналог указателя const T*), то вам понадобится константный итератор:
std::vector<int> vec;
...
const std::vector<int>::iterator iter = // iter работает как T* const
vec.begin();
*iter = 10; // Ok, изменяется то, на что
// указывает iter
++iter; // ошибка! iter константный
std::vector<int>::const_iterator citer = // citer работает как const T*
vec.begin();
*citer = 10; // ошибка! *citer константный
++citer; // нормально, citer изменяется
Некоторые из наиболее интересных применений const связаны с объявлениями функций.