|
|||||||
Константные функции-члены
Время создания: 29.03.2014 20:22
Текстовые метки: c++, const, функция, члены, класс, mutable
Раздел: Компьютер - Программирование - Язык C++ (Си++) - Главы книги "Эффективное использование C++"
Запись: xintrea/mytetra_syncro/master/base/1396110162izmml8tylt/text.html на raw.github.com
|
|||||||
|
|||||||
Назначение модификатора const в объявлении функций-членов – это определить, какие из них можно вызывать для константных объектов. Такие функции-члены важны по двум причинам. Во-первых, они облегчают понимание интерфейса класса, ведь полезно сразу видеть, какие функции могут модифицировать объект, а какие нет. Во-вторых, они обеспечивают возможность работать с константными объектами. Это очень важно для написания эффективного кода, потому что, как объясняется в правиле 20, один из основных способов повысить производительность программ на C++ – передавать объекты по ссылке на константу. Но эта техника будет работать только в случае, когда функции-члены для манипулирования константными объектами объявлены с модификатором const. Многие упускают из виду, что функции, отличающиеся только наличием const в объявлении, могут быть перегружены. Это, однако, важное свойство C++. Рассмотрим класс, представляющий блок текста: class TextBlock { public: ... const char& operator[](std::size_t position) const // operator[] для {return text[position];} // константных объектов char& operator[](std::size_t position) // operator[] для {return text[position];} // неконстантных объектов private: std::string text; }; Функцию operator[] в классе TextBlock можно использовать следующим образом: TextBlock tb(“Hello”); Std::cout << tb[0]; // вызов неконстантного // оператора TextBlock::operator[] const TextBlock ctb(“World”); Std::cout << ctb[0]; // вызов константного // оператора TextBlock::operator[] Кстати, константные объекты чаще всего встречаются в реальных программах в результате передачи по указателю или ссылке на константу. Приведенный выше пример ctb является довольно искусственным. Но вот вам более реалистичный: void print(const TextBlock& ctb) // в этой функции ctb – ссылка // на константный объект { std::cout << ctb[0]; // вызов const TextBlock::operator[] ... } Перегружая operator[] и создавая различные версии с разными возвращаемыми типами, вы можете по-разному обрабатывать константные и неконстантные объекты TextBlock: std::cout << tb[0]; // нормально – читается // неконстантный TextBlock tb[0] = ‘x’; // нормально – пишется // неконстантный TextBlock std::cout << ctb[0]; // нормально – читается // константный TextBlock ctb[0] = ‘x’; // ошибка! – запись // константного TextBlock Отметим, что ошибка здесь связана только с типом значения, возвращаемого operator[]; сам вызов operator[] проходит нормально. Причина ошибки – в попытке присвоить значение объекту типа const char&, потому что это именно такой тип возвращается константной версией operator[]. Отметим также, что тип, возвращаемый неконстантной версией operator[], – это ссылка на char, а не сам char. Если бы operator[] возвращал просто char, то следующее предложение не скомпилировалось бы: tb[0] = ‘x’; Это объясняется тем, что возвращаемое функцией значение встроенного типа модифицировать некорректно. Даже если бы это было допустимо, тот факт, что C++ возвращает объекты по значению (см. правило 20), означал бы следующее: модифицировалась копия tb.text[0], а не само значение tb.text[0]. Вряд ли это то, чего вы ожидаете. Давайте немного передохнем и пофилософствуем. Что означает для функции-члена быть константной? Существует два широко распространенных понятия: побитовая константность (также известная как физическая константность) и логическая константность. Сторонники побитовой константности полагают, что функция-член константна тогда и только тогда, когда она не модифицирует никакие данные-члены объекта (за исключением статических), то есть не модифицирует ни одного бита внутри объекта. Определение побитовой константности хорошо тем, что ее нарушение легко обнаружить: компилятор просто ищет присваивания членам класса. Фактически, побитовая константность – это константность, определенная в C++: функция-член с модификатором const не может модифицировать нестатические данные-члены объекта, для которого она вызвана. К сожалению, многие функции-члены, которые ведут себя далеко не константно, проходят побитовый тест. В частности, функция-член, которая модифицирует то, на что указывает указатель, часто не ведет себя как константная. Но если объекту принадлежит только указатель, то функция формально является побитово константной, и компилятор не станет возражать. Это может привести к неожиданному поведению. Например, предположим, что есть класс подобный Text-Block, где данные хранятся в строках типа char * вместо string, поскольку это необходимо для передачи в функции, написанные на языке C, который не понимает, что такое объекты типа string. class CtextBlock { public: char& operator[](std::size_t position) const // неудачное (но побитово { return pText[position]} // константное) // объявление operator[] private: char *pText; }; В этом классе функция operator[] (неправильно!) объявлена как константная функция-член, хотя она возвращает ссылку на внутренние данные объекта (эта тема обсуждается в правиле 28). Оставим это пока в стороне и отметим, что реализация operator[] никак не модифицирует pText. В результате компилятор спокойно сгенерирует код для функции operator[]. Ведь она действительно является побитово константной, а это все, что компилятор может проверить. Но посмотрите, что происходит: const CtextBlock cctb(“Hello”); // объявление константного объекта char &pc = &cctb[0]; // вызов const operator[] для получения // указателя на данные cctb *pc = ‘j’; // cctb теперь имеет значение “Jello” Несомненно, есть что-то некорректное в том, что вы создаете константный объект с определенным значением, вызываете для него только константную функцию-член и тем не менее изменяете его значение! Это приводит нас к понятию логической константности. Сторонники этой философии утверждают, что функции-члены с const могут модифицировать некоторые биты вызвавшего их объекта, но только так, чтобы пользователь не мог этого обнаружить. Например, ваш класс CTextBlock мог бы кэшировать длину текстового блока при каждом запросе: Class CtextBlock { public: ... std::size_t length() const; private: char *pText; std::size_t textLength; // последнее вычисленное значение длины // текстового блока bool lengthIsValid; // корректна ли длина в данный момент }; std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // ошибка! Нельзя присваивать lengthIsValid = true; // значение textLength и } // lengthIsValid в константной функции-члене return textLength; } Эта реализация length(), конечно же, не является побитово константной, поскольку может модифицировать значения членов textLength и lengthlsValid. Но в то же время со стороны кажется, что константности объектов CTextBlock это не угрожает. Однако компилятор не согласен. Он настаивает на побитовой константности. Что делать? Решение простое: используйте модификатор mutable. Он освобождает нестатические данные-члены от ограничений побитовой константности: Class CtextBlock { public: std::size_t length() const; private: char *pText; mutable std::size_t textLength; // Эти данные-члены всегда могут быть mutable bool lengthIsValid; // модифицированы, даже в константных }; // функциях-членах std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // теперь порядок lengthIsValid = true; // здесь то же } return textLength; } |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|