Запрос «const в C++» — один из самых частых у начинающих. Константность (const correctness) помогает сделать код безопаснее: защищает от случайной модификации данных, упрощает оптимизацию и улучшает читаемость API. Ниже — руководство с примерами и советами для реальных проектов.

const в C++: понятное руководство по константности (const correctness) с примерами

Запрос «const в C++» — один из самых частых у начинающих. Константность (const correctness) помогает сделать код безопаснее: защищает от случайной модификации данных, упрощает оптимизацию и улучшает читаемость API. Ниже — руководство с примерами и советами для реальных проектов.

Что такое const в C++

Ключевое слово const делает объект неизменяемым в пределах области видимости. Примеры:

const int a = 10;        // a нельзя изменить
int const b = 20;        // одинаково с const int b

// Важно: const влияет на тип.
// top-level const — константность самого объекта;
// low-level const — константность того, на что указывает указатель/ссылка.

const и указатели: частые вопросы

Правило чтения: const «приклеен» к тому, что справа. Если справа ничего — смотрим слева.

int x = 1, y = 2;

const int* p1 = &x   // pointer to const int (нельзя менять *p1, но можно p1)
int* const p2 = &x   // const pointer to int (нельзя менять p2, но можно *p2)
const int* const p3 = &x // const pointer to const int

*p1 = 42;   // ОШИБКА: *p1 — const
p1 = &y    // OK

*p2 = 42;   // OK
p2 = &y    // ОШИБКА: p2 — const

Ссылки всегда «константа-ссылка» (их нельзя переназначить), поэтому чаще пишут const T& для «только чтения» без копирования.

Константные методы и перегрузка по const

Метод с суффиксом const обещает не менять логическое состояние объекта (все поля, кроме помеченных как mutable).

struct Vec2 {
    double x{0}, y{0};

    double length() const {          // метод "только чтение"
        return std::sqrt(x*x + y*y);
    }

    void setX(double nx) { x = nx; } // изменяет объект
};

void demo() {
    const Vec2 v{3, 4};
    double L = v.length(); // OK
    // v.setX(10);         // ОШИБКА: метод не const
}

Можно перегружать методы по константности — удобно, когда для const-объекта возвращаем "чтение", а для неконстантного — "чтение+запись":

class Buffer {
    std::vector<int> data;
public:
    const int& operator[](size_t i) const { return data[i]; }
    int&       operator[](size_t i)       { return data[i]; }
};

void use(Buffer& b, const Buffer& cb) {
    int a = cb[0];   // возвращает const int&
    b[0] = 42;       // возвращает int& — можно менять
}

Параметры и возвращаемые значения

  • Мелкие типы (int, double, enum, указатели) передавайте по значению.
  • Крупные объекты — по const T& для избежания копирования.
  • Возвращать по значению — нормально; добавлять const к возвращаемому значению, как правило, бессмысленно.
// Хорошо: без лишних копий и с ясной семантикой
std::string_view Find(const std::string& s, char ch);

// Избыточно: const при возврате по значению не даёт пользы
const std::string MakeName(); // лучше просто std::string

Если нужна «глубокая» неизменяемость результата — возвращайте const T& только если гарантируете время жизни объекта (часто это член класса или статик).

const и STL: итераторы и диапазоны

Используйте const-итераторы и cbegin/cend для обхода без модификации:

std::vector<int> v{1,2,3};

for (auto it = v.cbegin(); it != v.cend(); ++it) {
    // *it — const int&
}

for (const auto& value : v) { // range-based for + const auto&
    // value — const int&
}

mutable и const_cast: когда уместно

Иногда «константный» метод хочет кэшировать результат. Для этого есть mutable — поле можно менять даже в const-методе. Делайте так только для прозрачных оптимизаций.

class Heavy {
    mutable bool cached{false};
    mutable double memo{0};
public:
    double value() const {
        if (!cached) {
            memo = /* тяжёлый расчёт */ 42.0;
            cached = true;
        }
        return memo;
    }
};

const_cast снимает константность, но использовать его нужно крайне осторожно. Если вы меняете объект, который изначально был действительно const, — поведение неопределено.

void f(const int* p) {
    // ПЛОХО: если p указывает на действительно const-объект
    int* q = const_cast<int*>(p);
    // *q = 123; // может привести к UB
}

constexpr vs const

  • const — неизменяемость во время выполнения.
  • constexpr — значение вычисляется на этапе компиляции (если возможно). Любой constexpr по определению также const.
const double Pi = 3.1415926535;     // константа во время выполнения
constexpr int N = 2 * 3 * 5;        // вычислено на этапе компиляции

Мини-пример: конфиг только для чтения

Соберём простой класс, где const correctness делает интерфейс очевидным.

class Config {
    std::map<std::string, std::string> kv;
public:
    explicit Config(std::map<std::string, std::string> init)
        : kv(std::move(init)) {}

    // Чтение без изменений
    const std::string& get(const std::string& key) const {
        static const std::string empty;
        if (auto it = kv.find(key); it != kv.end()) return it->second;
        return empty;
    }

    // Изменение доступно только через неконстантный метод
    void set(std::string key, std::string value) {
        kv[std::move(key)] = std::move(value);
    }
};

void demoConfig() {
    Config cfg({{"mode","dev"},{"port","8080"}});
    const Config& r = cfg;

    // r.set("mode","prod"); // ОШИБКА: у const-ссылки нет set
    auto mode = r.get("mode");  // Только чтение — так и задумывалось

    cfg.set("mode","prod");    // Изменять может только неконстантный объект
}

Частые ошибки и лучшие практики

  • Не добавляйте const к возвращаемому по значению типу без необходимости: это не усиливает защиту и мешает перемещению/присваиванию у временных.
  • Предпочитайте const T& в параметрах для тяжёлых объектов; для мелких — значение.
  • Помечайте методы как const, если они логически не меняют состояние — это документирует намерение и раскрывает больше оптимизаций.
  • Используйте cbegin/cend и const-итераторы для обхода «только чтение».
  • Не злоупотребляйте mutable и const_cast — они для редких случаев (кэш, внутриклассная синхронизация).
  • Проектируйте API «по умолчанию неизменяемыми»: чем меньше точек изменения, тем проще тестировать и сопровождать.

Что дальше изучить