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

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

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 «по умолчанию неизменяемыми»: чем меньше точек изменения, тем проще тестировать и сопровождать.
  • Что дальше изучить

    Константность тесно связана с правилами владения и временем жизни. Освоив const correctness, вы легче разберётесь с RAII, правилами трёх/пяти/нуля и безопасными абстракциями. Хотите структурированную практику, домашние задания и обратную связь? Попробуйте интенсив: Записаться на бесплатный модуль «Программирование на C++ с Нуля до Гуру» — отличный старт, чтобы уверенно применять const в реальном коде.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий