Константные функции-члены

Константные функции-члены

Назначение модификатора 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;

}

Данный текст является ознакомительным фрагментом.