Course program← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Константность

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 в неконстантной версии возвращает ссылку наружу, через которую объект изменят, – такой метод константным быть не может.