Скотт Майерс - Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Многие упускают из виду, что функции, отличающиеся только наличием 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;
}
Как избежать дублирования в константных и неконстантных функциях-членах
Использование 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[] должна вызывать другую. И это подводит нас к вопросу об отбрасывании константности.
Откройте для себя мир чтения на siteknig.com - месте, где каждая книга оживает прямо в браузере. Здесь вас уже ждёт произведение Скотт Майерс - Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ, относящееся к жанру Программирование. Никаких регистраций, никаких преград - только вы и история, доступная в полном формате. Наш литературный портал создан для тех, кто любит комфорт: хотите читать с телефона - пожалуйста; предпочитаете ноутбук - идеально! Все книги открываются моментально и представлены полностью, без сокращений и скрытых страниц. Каталог жанров поможет вам быстро найти что-то по настроению: увлекательный роман, динамичное фэнтези, глубокую классику или лёгкое чтение перед сном. Мы ежедневно расширяем библиотеку, добавляя новые произведения, чтобы вам всегда было что открыть "на потом". Сегодня на siteknig.com доступно более 200000 книг - и каждая готова стать вашей новой любимой. Просто выбирайте, открывайте и наслаждайтесь чтением там, где вам удобно.


