Константные методы классов с динамической памятью

by Sergey Afonin

Ключевое слово const позволяет определять методы, которые не могут изменять состояние объекта. Этот механизм введен исключительно для удобства программиста, а не для какой-либо защиты приложения злоумышленника (в C++ есть стандартный способ обхода запрета на иззменение константных объектов, которым не следует пользоваться). Если по логике работы объект или какой-либо параметр метода не должны изменяться при выполнении метода, то желательно объявить метод и/или его параметры константными. Например, класс Matrix для работы с матрицами может включать методы умножения матрицы на вектор (mult) и оператор *=, который умножает матрицу на другую (с изменением матрицы):

std::vector<double> mult(const std::vector<double>& vect) const;
Matrix& operator*= (const Matrix& m2); 
Метод mult объявлен константным. Это означает, что после его вызова сама матрица не изменится. Параметром этого метода является ссылка на константный вектор, то есть вызов mult не может привести к изменению вектора. Оператор *= может изменить саму матрицу, но не свой аргумент. Использование const имеет как минимум два приемущества. Это позволяет: В случае, когда состояние объекта выключает области памяти, выделенные с помощью оператора new, простого указания модификатора const может оказаться недостаточно для корректной работы программы.

Формальное и логическое состояние объекта

Использование модификатора const в сигнатурах методов позволяет явно указать, что изменение объекта не предполагается. В этим случае компилятор пытается проверить, что реализация метода действительно не модифицирует состояние объекта. Однако в случае, если объект использует динамическую памать, автоматическая проверка становится невозможной. Рассмотрим простой класс Vec, который создает динамическй массив и определяет оператор [] взятия элемента массива.

#include <iostream>

class Vec {
    int *data_;
    size_t len_;
    public:
        Vec(size_t len) { data_ = new int [len_ = len]; }
        ~Vec() { delete[] data_; }
        int& operator[](size_t i) const { return data_[i]; }
};

int main() {
    const Vec const_vec(3);
    std::cout << const_vec[1] << std::endl; // 0
    const_vec[1] = 1;
    std::cout << const_vec[1] << std::endl; // 1 !!!

    return 0;
} 
Заметим, что operator[] объявлен константным, то есть мы предполагаем, что его вызов не должен приводить к изменению объекта Vec. Однако приведенная выше программа, которая создает константный объект const_vec и изменяет значение элемента вектора с индексом 1, успешно компилируется. В результате ее выполнения вектор const_vec, который был объявлен неизменяемым , изменяется.

На самом деле, с точки зрения компилятора ничего предосудительного не произошло. Состоянием объекта const_vec считается указатель data_ и целое число len_. Эти значения не изменились, поэтому программа считается формально корректной.

Отметим, что в случае статического объявления массива data_

class Vec {
  // ...
  int data_[3];
  // ...
  int& operator[](size_t i) const { return data_[i]; }
} 
компилятор выдаст сообщение об ошибке, но использование статических массивов возможно далеко не всегда.

Проверка неизменности логического состояния

Возможно ли модифицировать класс Vec таким образом, чтобы имея ссылку на константный объект было бы невозможно изменить элементы вектора?

Если оператор взятия индекса будет возвращать константную ссылку

const int& operator[](size_t i) const { return data_[i]; } 
то не удастся откомпилировать программу вида
Vec vec(3);
const_vec[1] = 1;
так как теперь запрещено использовать результат выполнения operator[] в качестве lvalue.

Решение

В языке C++ модификатор const включается в сигнатуру метода. Методы void f() const; и void f(); различаются. Первый вызывается в случае, когда ссылка на объект является константной, а второй — в случае неконстантной ссылки.

Корректная реализация класса Vec может включать две различные реализации оператора взятия индекса.

class Vec {
    int *data_;
    size_t len_;
    public:
        Vec(size_t len) { data_ = new int [len_ = len]; }
        ~Vec() { delete[] data_; }

        // Оператор [] для изменяемых объектов
        int& operator[](size_t i) { return data_[i]; }

        // Оператор [] для константных объектов 
        const int& operator[](size_t i) const { return data_[i]; }
};

Thanks for reading.