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;
};
Ох! Налицо все неприятности, связанные с дублированием кода: увеличение времени компиляции, размера программы и неудобство сопровождения. Конечно, можно переместить весь код для проверки выхода за границы массива и прочего в отдельную функцию-член (естественно, закрытую), которую будут вызывать обе версии operator[], но обращения к этой функции все же будут дублироваться.
В действительности было бы желательно реализовать функциональность 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 из типа возвращаемого значения.