KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » Скотт Майерс - Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ

Скотт Майерс - Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Скотт Майерс, "Эффективное использование 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[] должна вызывать другую. И это подводит нас к вопросу об отбрасывании константности.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*