побитово константной, и компилятор не станет возражать. Это может привести к неожиданному поведению. Например, предположим, что есть класс подобный 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;
}
Как избежать дублирования в константных и неконстантных функциях- членах
Использование mutable – замечательное решение проблемы, когда побитовая константность вас не вполне устраивает, но оно не устраняет всех трудностей, связанных с const. Например, представьте, что operator[] в классе TextBlock (и CTextBlock) не только возвращает ссылку на соответствующий символ, но также проверяет выход за пределы массива, протоколирует информацию о доступе и, возможно, даже проверяет целостность данных. Помещение всей этой логики в обе версии функции operator[] – константную и неконстантную (даже если забыть, что теперь мы имеем необычно длинные встроенные функции – см. правило 30) – приводит к такому вот неуклюжему коду:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const
{
... // выполнить проверку границ массива
... // протоколировать доступ к данным
... // проверить целостность данных
return text[position];
}
char& operator[](std::size_t position) const
{
... // выполнить проверку границ массива
... // протоколировать доступ к данным
... // проверить целостность данных
return text[position];
}
private:
std:string text;