This lesson is not translated yet – the Russian original is below.
const в C++ – не подсказка и не соглашение. Это часть типа, которую проверяет компилятор, и
единственный способ записать в сигнатуре обещание «я это не изменю».
Константная переменная
const int kMax = 100;
kMax = 200; // ошибка компиляции
const int kSize; // ошибка: константу обязательно инициализировать
Писать const слева или справа от типа – дело вкуса: const int и int const означают одно и
то же. Второй вариант читается последовательнее в сложных объявлениях, но первый распространён
шире.
const и указатели
Здесь начинается путаница, и разбирается она одним правилом: const относится к тому, что
слева от него; если слева ничего нет – к тому, что справа.
int value = 1;
int other = 2;
const int* a = &value; // указатель на константу
*a = 5; // ошибка: менять то, на что смотрим, нельзя
a = &other; // можно: сам указатель не константный
int* const b = &value; // константный указатель
*b = 5; // можно
b = &other; // ошибка
const int* const c = &value; // нельзя ни то, ни другое
Читать объявления удобно справа налево: int* const b – «b есть константный указатель на int»,
const int* a – «a есть указатель на константный int».
В сигнатурах функций почти всегда встречается первый вариант: void Print(const char* text) – «не буду менять символы». Константность самого параметра-указателя вызывающего не интересует: параметр и так копия.
Преобразования
Добавить константность можно всегда, убрать – нет:
int value = 1;
int* p = &value;
const int* cp = p; // можно: обещаем меньше, чем можем
int* back = cp; // ошибка: обещание нельзя молча забрать назад
А вот на втором уровне вложенности добавлять уже нельзя:
int* p = nullptr;
const int** pp = &p; // ошибка компиляции
Кажется избыточным, но запрет спасает всю систему:
const int i = 1;
int* p = nullptr;
const int** pp = &p; // если бы это было разрешено...
*pp = &i; // ...то это законно: слева тип const int*
*p = 2; // ...и константа i изменена
Одна разрешённая строчка ломает гарантию целиком, поэтому её и запретили.
Инструмент для снятия константности всё же есть – const_cast:
void LegacyApi(char* text); // не меняет строку, но об этом не написано
void Call(const std::string& s) {
LegacyApi(const_cast<char*>(s.c_str())); // единственный законный повод
}
Законный повод здесь один: старый API, который на самом деле объект не меняет, но об этом не сообщает. Если функция всё-таки изменит объект, который изначально создавался константным, – это неопределённое поведение, а не «сработает». const_cast в коде – всегда повод спросить, откуда взялась константность и не проще ли починить её источник.
Константные методы
class String {
public:
size_t Length() const { return size_; } // не меняет объект
void Append(const String& other); // меняет
private:
size_t size_;
char* str_;
};
Внутри const-метода this имеет тип const String*, поэтому присвоить полю не получится.
Практическое следствие уже знакомо: у константного объекта доступны только константные методы.
А объекты приходят в функции как const T& почти всегда – значит, метод без const там
недоступен.
void Show(const String& s) {
std::cout << s.Length(); // работает, только если Length помечен const
}
Отсюда термин const-корректность: код, в котором все не изменяющие методы помечены, а все не изменяемые параметры переданы по const&. Свойство это «всё или ничего»: одна непомеченная функция в середине цепочки вынуждает снимать const вверх по всему стеку вызовов. Поэтому const ставят сразу, а не «когда понадобится».
Логическая и физическая константность
Компилятор проверяет физическую константность: биты объекта не меняются. Иногда нужна логическая: наблюдаемое поведение не меняется, а внутри что-то происходит – например, кэшируется результат.
Для этого есть mutable:
class Document {
public:
size_t WordCount() const {
if (!counted_) {
count_ = ComputeWordCount(); // менять можно: поле mutable
counted_ = true;
}
return count_;
}
private:
mutable size_t count_ = 0;
mutable bool counted_ = false;
};
Снаружи метод остаётся честно константным: сколько раз его ни вызови, результат тот же.
mutable уместен для кэшей, счётчиков обращений и мьютексов – и подозрителен во всех остальных
случаях.
Перегрузка по константности
Константность – часть сигнатуры, поэтому методы можно перегружать по ней:
class String {
public:
char& At(size_t i) { return str_[i]; } // для неконстантного объекта
char At(size_t i) const { return str_[i]; } // для константного
private:
char* str_;
};
String s("hello");
s.At(0) = 'H'; // вызвана первая: вернула ссылку, можно писать
const String c("hello");
char ch = c.At(0); // вызвана вторая: вернула копию
Так устроены operator[] и at() у всех стандартных контейнеров. Выбор делает компилятор по
константности объекта, у которого метод вызван, – никаких проверок во время выполнения.
Какие методы класса String стоит пометить const? Length – да, она только читает. Equal –
да, сравнение ничего не меняет. Append – нет, он дописывает. А вот At в неконстантной версии
возвращает ссылку наружу, через которую объект изменят, – такой метод константным быть не может.