12.12 – Константные объекты классов и функции-члены

Добавлено 7 июля 2021 в 22:29

В уроке «4.14 – const, constexpr и символьные константы» вы узнали, что переменные базовых типов данных (int, double, char и т.д.) можно сделать константными с помощью ключевого слова const, и что все константные переменные должны быть инициализированы во время создания.

В случае константных переменных базовых типов данных инициализация может быть выполнена путем копирующей, прямой или унифицированной инициализации:

const int value1 = 5;   // копирующая инициализация
const int value2(7);    // прямая инициализация
const int value3 { 9 }; // унифицированная инициализация (C++11)

const и классы

Точно так же с помощью ключевого слова const можно сделать константными экземпляры объектов класса. Инициализация выполняется с помощью конструкторов классов:

const Date date1;                  // инициализируем с использованием конструктора по умолчанию
const Date date2(2020, 10, 16);    // инициализируем с помощью конструктора с параметрами
const Date date3 { 2020, 10, 16 }; // инициализируем с помощью конструктора с параметрами (C++11)

После того, как константный объект класса был инициализирован с помощью конструктора, любая попытка изменить переменные-члены объекта запрещается, так как это нарушит константность объекта. Это включает в себя как изменение переменных-членов напрямую (если они являются открытыми), так и вызов функций-членов, которые устанавливают значения переменных-членов. Рассмотрим следующий класс:

class Something
{
public:
    int m_value;
 
    Something(): m_value{0} { }
 
    void setValue(int value) { m_value = value; }
    int getValue() { return m_value ; }
};
 
int main()
{
    const Something something{}; // вызывает конструктор по умолчанию
 
    something.m_value = 5; // ошибка компиляции: нарушает константность
    something.setValue(5); // ошибка компиляции: нарушает константность
 
    return 0;
}

Обе приведенные выше строки, включающие переменную something, являются недопустимыми, потому что они нарушают константность something, пытаясь либо изменить переменную-член напрямую, либо вызывая функцию-член, которая пытается изменить переменную-член.

Как и в случае с обычными переменными, объекты класса константными обычно необходимо делать, если вам нужно убедиться, что они не изменяются после создания.

Константные функции-члены

Теперь рассмотрим следующую строку кода:

std::cout << something.getValue();

Как ни странно, это также вызовет ошибку компиляции, даже если getValue() ничего не делает для изменения переменной-члена! Оказывается, константные объекты класса могут явно вызывать только константные функции-члены, а getValue() не был помечена как константная функция-член.

Константная функция-член – это функция-член, которая гарантирует, что она не будет изменять объект или вызывать какие-либо неконстантные функции-члены (поскольку они могут изменять объект).

Чтобы сделать getValue() константной функцией-членом, мы просто добавляем ключевое слово const к прототипу функции после списка параметров, но перед телом функции:

class Something
{
public:
    int m_value;
 
    Something(): m_value{0} { }
 
    void resetValue() { m_value = 0; }
    void setValue(int value) { m_value = value; }
 
    // обратите внимание на добавление ключевого слова const
    // после списка параметров, но перед телом функции
    int getValue() const { return m_value; } 
};

Теперь getValue() стала константной функцией-членом, что означает, что мы можем вызывать ее для любых константных объектов.

Для функций-членов, определенных вне определения класса, ключевое слово const должно использоваться как в прототипе функции в определении класса, так и в определении функции:

class Something
{
public:
    int m_value;
 
    Something(): m_value{0} { }
 
    void resetValue() { m_value = 0; }
    void setValue(int value) { m_value = value; }
 
    // обратите внимание на добавление ключевого слова const здесь
    int getValue() const; 
};
 
int Something::getValue() const // и здесь
{
    return m_value;
}

Более того, любая константная функция-член, которая пытается изменить переменную-член или вызвать неконстантную функцию-член, вызовет ошибку компиляции. Например:

class Something
{
public:
    int m_value ;
 
    // ошибка компиляции, константные функции
    // не могут изменять переменные-члены.
    void resetValue() const { m_value = 0; } 
};

В этом примере resetValue() отмечена как константная функция-член, но пытается изменить m_value. Это вызовет ошибку компиляции.

Обратите внимание, что конструкторы не могут быть помечены как const. Это связано с тем, что конструкторы должны иметь возможность инициализировать свои переменные-члены, а константный конструктор не сможет этого сделать. Следовательно, язык запрещает константные конструкторы.

Константные функции-члены также могут вызываться неконстантными объектами.

Правило


Делайте любую функцию-член, которая не изменяет состояние объекта класса, константной, чтобы она могла вызываться константными объектами.

Получение константных объектов через передачу по константной ссылке

Хотя одним из способов создания константных объектов является создание константных экземпляров объектов класса, но более распространенным способом получения константного объекта является передача объекта в функцию по константной ссылке.

В уроке о передаче аргументов по ссылке мы рассмотрели достоинства передачи аргументов типа класса по константной ссылке, а не по значению. Напомним, передача аргумента типа класса по значению приводит к созданию копии экземпляра класса (что происходит медленно) – в большинстве случаев нам не нужна копия, ссылка на исходный аргумент работает нормально и является более производительной, поскольку позволяет избежать ненужных копирований. Обычно мы делаем ссылку константной, чтобы гарантировать, что функция непреднамеренно не изменит аргумент, и чтобы функция могла работать с r-значениями (rvalue) (например, с литералами), которые могут передаваться как константные ссылки, но не могут как неконстантные ссылки.

Можете ли вы понять, что не так в следующем коде?

#include <iostream>
 
class Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
public:
    Date(int year, int month, int day)
    {
        setDate(year, month, day);
    }
 
    void setDate(int year, int month, int day)
    {
        m_year = year;
        m_month = month;
        m_day = day;
    }
 
    int getYear() { return m_year; }
    int getMonth() { return m_month; }
    int getDay() { return m_day; }
};
 
// обратите внимание: здесь мы передаем дату по константной ссылке,
// чтобы избежать копирования
void printDate(const Date &date)
{
    std::cout << date.getYear() << '/' << date.getMonth() << '/' << date.getDay() << '\n';
}
 
int main()
{
    Date date{2016, 10, 16};
    printDate(date);
 
    return 0;
}

Ответ заключается в том, что внутри функции printDate дата обрабатывается как константный объект. И с этой константной датой мы вызываем функции getYear(), getMonth() и getDay(), которые не являются константными. Поскольку мы не можем вызывать неконстантные функции-члены для константных объектов, это вызовет ошибку компиляции.

Исправление простое: сделайте getYear(), getMonth() и getDay() константными:

class Date
{
private:
    int m_year;
    int m_month;
    int m_day;
 
public:
    Date(int year, int month, int day)
    {
        setDate(year, month, day);
    }
 
    // setDate() не может быть константной,
    // поскольку изменяет переменные-члены
    void setDate(int year, int month, int day)
    {
        m_year = year;
        m_month = month;
        m_day = day;
    }
 
    // Все следующие геттеры можно сделать константными
    int getYear() const { return m_year; }
    int getMonth() const { return m_month; }
    int getDay() const { return m_day; }
};

Теперь в функции printDate() для константной даты можно успешно вызывать getYear(), getMonth() и getDay().

Константные члены не могут возвращать неконстантные ссылки на члены

Когда функция-член является константной, скрытый указатель *this также является константным, что означает, что все члены в этой функции рассматриваются как константы. Следовательно, константная функция-член не может возвращать неконстантную ссылку на член, поскольку для этого потребуется отбросить константность этого члена. Константные функции-члены могут возвращать константные ссылки на члены.

Мы увидим пример этого в следующем разделе.

Константная и неконстантная перегрузка функции

Наконец, хотя это делается не очень часто, но можно перегрузить функцию таким образом, чтобы получить константную и неконстантную версии одной и той же функции. Это работает, потому что квалификатор const считается частью сигнатуры функции, поэтому две функции, которые отличаются только своей константностью, считаются разными.

#include <string>
 
class Something
{
private:
    std::string m_value;
 
public:
    Something(const std::string &value=""): m_value{ value } {}
 
    // getValue() для константных объектов (возвращает константную ссылку)
    const std::string& getValue() const { return m_value; } 
    // getValue() для неконстантных объектов (возвращает неконстантную ссылку)
    std::string& getValue() { return m_value; } 
};

Константная версия функции будет вызываться для любых константных объектов, а неконстантная версия будет вызываться для любых неконстантных объектов:

int main()
{
	Something something{};
	something.getValue() = "Hi"; // вызывает неконстантную getValue();
 
	const Something something2{};
	something2.getValue(); // вызывает константную getValue();
 
	return 0;
}

Перегрузка функции с константной и неконстантной версиями обычно выполняется, когда возвращаемое значение должно отличаться константностью. В приведенном выше примере неконстантная версия getValue() будет работать только с неконстантными объектами, но она более гибкая, поскольку мы можем использовать ее как для чтения, так и для записи m_value (что мы и делаем, присваивая строку "Hi" ).

Константная версия getValue() будет работать как с константными, так и с неконстантными объектами, но возвращает константную ссылку, чтобы гарантировать, что мы не сможем изменить данные константного объекта.

Резюме

Поскольку передача объектов по константной ссылке является обычным явлением, ваши классы должны быть совместимы с константами. Это означает задание любой функции-члена, которая не изменяет состояние объекта класса, константной!

Теги

C++ / CppconstLearnCppДля начинающихКласс (программирование)ОбучениеПерегрузка (программирование)Программирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.