Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 5 6 7 8 9 10 11 12 13 ... 73
Перейти на страницу:

В действительности было бы желательно реализовать функциональность operator[] один раз, а использовать в двух местах. То есть одна версия operator[] должна вызывать другую. И это подводит нас к вопросу об отбрасывании константности.

С самого начала отметим, отбрасывать константность нехорошо. Я посвятил целое правило 27 тому, чтобы убедить вас не делать этого, но дублирование кода – тоже не сахар. В данном случае константная версия operator[] делает в точности то же самое, что неконстантная, и отличие между ними – лишь в присутствии модификатора const. В этой ситуации отбрасывать const безопасно, поскольку пользователь, вызывающий неконстантный operator[], так или иначе должен получить неконстантный объект. Ведь в противном случае он не стал бы вызывать неконстантную функцию. Поэтому реализация неконстантного operator[] путем вызова константной версии – это безопасный способ избежать дублирования кода, даже пусть даже для этого требуется воспользоваться оператором const_cast. Ниже приведен получающийся в результате код, но он станет яснее после того, как вы прочитаете следующие далее объяснения:

class TextBlock {

public:

...

const char& operator[](std::size_t position) const // то же, что и раньше

{

...

...

...

return text[position];

}

char& operator[](std::size_t position) const // теперь просто

// вызываем const op[]

{

return

const_cast<char&>( // из возвращаемого типа

// op[] исключить const

static_cast<const TextBlock&>(*this) // добавить const типу

// *this

[position] // вызвать константную

); // версию op[]

}

...

};

Как видите, код включает два приведения, а не одно. Мы хотим, чтобы неконстантный operator[] вызывал константный, но если внутри неконстантного оператора [] просто вызовем operator[], то получится рекурсивный вызов. Во избежание бесконечной рекурсии нужно указать, что мы хотим вызвать const operator[], но прямого способа сделать это не существует. Поэтому мы приводим *this от типа TextBlock& к const TextBlock&. Да, мы выполняем приведение, чтобы добавить константность! Таким образом, мы имеем два приведения: одно добавляет константность *this (чтобы был вызван const operator[]), а второе – исключает const из типа возвращаемого значения.

Приведение, которое добавляет const, выполняет безопасное преобразование (от неконстантного объекта к константному), поэтому мы используем для этой цели static_cast. Приведение же, которое отбрасывает const, может быть выполнено только с помощью const_cast, поэтому у нас здесь нет выбора. (Строго говоря, выбор есть. Приведение в стиле C также работает, но, как я объясняю в правиле 27, такие приведения редко являются правильным рещением. Если вы не знакомы с операторами static_cast или const_cast, прочитайте о них в правиле 27.)

Помимо всего прочего, в этом примере мы вызываем оператор, поэтому синтаксис выглядит немного странно. Возможно, этот код не займет приз на конкурсе красоты, зато позволяет достичь нужного эффекта – избежать дублирования посредством реализации неконстантной версии operator[] в терминах константной. И хотя для достижения цели пришлось воспользоваться неуклюжим синтаксисом, который сможете понять только вы сами, однако техника реализации неконстантных функций-членов через неконстантные определенно заслуживает того, чтобы ее знать.

А еще нужно иметь в виду, что решать эту задачу наоборот – путем вызова неконстантной версии из константной – неправильно. Помните, что константная функция-член обещает никогда не изменять логическое состояние объекта, а неконстантная не дает таких гарантий. Если вы вызовете неконстантную функцию из константной, то рискуете получить ситуацию, когда объект, который не должен модифицироваться, будет изменен. Вот почему этого не следует делать: чтобы объект не изменился. Фактически, чтобы получить компилируемый код, вам пришлось бы использовать const_cast для отбрасывания константности *this, а это явный признак неудачного решения. Обратная последовательность вызовов – такая, как описана выше, – безопасна. Неконстантная функция-член может делать все, что захочет с объектом, поэтому вызов из нее константной функции-члена ничем не грозит. Потому-то мы и применяем к *this оператор static_cast, отбрасывания константности при этом не происходит.

Как я уже упоминал в начале этого правила, модификатор const – чудесная вещь. Для указателей и итераторов; для объектов, на которые ссылаются указатели, итераторы и ссылки; для параметров функций и возвращаемых ими значений; для локальных переменных, для функций-членов – всюду const ваш мощный союзник. Используйте его, где только возможно. Вам понравится!

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

• Объявление чего-либо с модификатором const помогает компиляторам обнаруживать ошибки. const можно использовать с объектами в любой области действия, с параметрами функций и возвращаемых значений, а также с функциями-членами в целом.

• Компиляторы проверяют побитовую константность, но вы должны программировать, применяя логическую константность.

• Когда константные и неконстантные функции-члены имеют, по сути, одинаковую реализацию, то дублирования кода можно избежать, заставив неконстантную версию вызывать константную.

Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы

Отношение C++ к инициализации значений объектов может показаться странным. Например, если вы пишете:

int x;

то в некоторых контекстах переменная x будет гарантированно инициализирована нулем, а в других – нет. Если вы пишете:

class Point {

int x, y;

};

...

Point p;

то члены-данные объекта p иногда будут инициализированы (нулями), а иногда – нет. Если вы перешли к C++ от языка, где неинициализированные объекты не могут существовать, обратите на это внимание.

Чтение неинициализированных значений может быть причиной неопределенного поведения. На некоторых платформах такое простое действие, как доступ к неинициированному значению для чтения, может вызвать аварийную остановку программы. Но чаще вы получите случайный набор битов, который испортит внутреннее состояние объекта, в который они записываются, и в конечном итоге это приведет к необъяснимому поведению программы и длительному поиску ошибки в отладчике.

Сформулируем правила, которые описывают, когда инициализация объекта гарантируется, а когда нет. К сожалению, эти правила достаточно сложны – на мой взгляд, слишком сложны, чтобы их стоило запоминать. Вообще, если вы работаете с C-частью C++ (см. правило 1) и инициализация может стоить определенных затрат во время исполнения, то не гарантируется, что она произойдет. Это объясняет, почему содержимое массивов (в C-части C++) не обязательно инициализируется, а содержимое вектора (из STL-части C++) инициализируется всегда.

По-видимому, лучший способ поведения в такой неопределенной ситуации – всегда инициализировать объекты, прежде чем их использовать. Для объектов встроенных типов, не являющихся членами классов, это нужно делать вручную. Например:

int x = 0; // ручная инициализация int

const char * text = “Строка в стиле C”; // ручная инициализация указателя

// (см. также правило 3)

double d; // «инициализация» чтением

std::cin >> d; // из входного потока

Почти во всех остальных случаях ответственность за инициализацию ложится на конструкторы. Правило простое: убедитесь, что все конструкторы инициализируют в объекте всё.

Этому правилу легко следовать, но важно не путать присваивание с инициализацией. Рассмотрим конструктор класса, представляющего записи в адресной книге:

class PhoneNumber {…}

class ABEntry { // ABEntry = “Address Book Entry”

public:

ABEntry(const std::string& name, const std::string& address,

const std::list<PhoneNumber>& phones);

private:

std::string theName;

std::string theAddress;

std::list<PhoneNumber> thePhones;

int numTimesConsulted;

};

ABEntry(const std::string& name, const std::string& address,

const std::list<PhoneNumber>& phones)

{

theName = name; // все это присваивание, а не инициализация

theAddress = address;

thePhones = phones;

numTimesConsulted = 0;

}

Да, в результате порождаются объекты ABEntry со значениями, которых вы ожидаете, но это все же не лучший подход. Правила C++ оговаривают, что члены объекта инициируются перед входом в тело конструктора. То есть внутри конструктора ABEntry члены theName, theAddress и thePhones не инициализируются, а им присваиваются значения. Инициализация происходит ранее: когда автоматически вызываются их конструкторы перед входом в тело конструктора ABEntry. Это не касается numTimesConsulted, поскольку этот член относится к встроенному типу. Для него нет никаких гарантий того, что он вообще будет инициализирован перед присваиванием.

1 ... 5 6 7 8 9 10 11 12 13 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии