IT Notes

Как пользоваться const в C++?

Введение

С помощью ключевого слова const в C++ можно объявлять не только константы. Оно имеет много значений. И не лишним будем понимать каждое из них. Это позволяет писать более надежный код, указывая компилятору на ограничения, которые он должен проверять. Таким образом, любая попытка неправильного использования переменной или функции, объявленной с использованием const, будет выявлена на самых ранних этапах.

Переменные и const

Наиболее простой и интуитивно понятный вариант использования const заключается в объявлении константных значений:

const int CONST_VAL = 0;
// CONST_VAL = 1; приводит к ошибке на этапе компиляции

Похожего результата можно добиться с помощью макросов:

#define CONST_VAL 0

Но вариант с макросом не столь хорош, поскольку не дает возможности указать тип переменной. Из-за возможных проблем с типизацией лучше пользоваться константами, а не макросами. Более того, макросов лучше и вовсе избегать. В C++ реализована прекрасная поддержка шаблонных функций и классов, которые в большинстве случаев представляют более надежную альтернативу. Однако стоит признать, что бывают случаи, когда макросы оказываются полезными. Но сейчас речь не о них.

Таким образом, если мы объявили переменную с const, то значение должно быть присвоено сразу. Потом уже ничего не сделать. А что, если нам надо объявить константный вектор? В C++11 для этого предусмотрена специальная конструкция:

const std::vector< int > CONST_VECTOR = { 1, 2, 3, 4 };

А что, если мы по какой-то причине не может пользоваться C++11? И в этом случае можно легко объявить константный вектор:

std::vector< int > makeVector() {
    std::vector< int > v;
    v.push_back( 1 );
    v.push_back( 2 );
    v.push_back( 3 );
    v.push_back( 4 );

    return v;
}

const std::vector< int > CONST_VECTOR = makeVector();

То есть для сложных типов, которые не получается заполнить в одну строку, достаточно вынести компоновку в отдельную функцию для инициализации. С другой стороны, мы могли бы объявить вектор в функции makeVector() с ключевым словом static и вместо константы использовать саму функцию. Но это уже дело вкуса:

std::vector< int > makeVector() {
    static std::vector< int > v;
    if( v.empty() ) {
        v.push_back( 1 );
        v.push_back( 2 );
        v.push_back( 3 );
        v.push_back( 4 );
    }

    return v;
}

Константная ссылка объявляется схожим образом:

int x = 0;
const int& xRef = x; // то же самое: int const& xRef = x;
// xRef = 1; приводит к ошибке на этапе компиляции

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

Для указателей существует три варианта использования const:

int x = 0;

// Вариант 1
const int* xPtr1 = &x; // то же самое: int const* xPtr1 = &x;
xPtr1 = NULL; // имеем право изменить то, на что будет указывать указатель
// *xPtr1 = 1; но нельзя изменить значение переменной, на которую указываем

// Вариант 2
int* const xPtr2 = &x;
// xPtr2 = NULL; нельзя изменять то, на что будет указывать указатель
*xPtr2 = 1; // но можем менять значение переменной, на которую он указывает

// Вариант 3
const int* const xPtr3 = &x;
// xPtr3 = NULL; ничего нельзя менять
// *xPtr3 = 1;

В варианте 1 мы получили указатель, который можно использовать как более гибкую константную ссылку. Он работает почти так же, но мы можем в любой момент сослаться на другую переменную. Вариант 2 работает так же, как обычная ссылка. Значение менять можно, а указать на другую переменную не выйдет. И наконец вариант 3. Он равносилен случаю константной ссылки. То есть один раз объявили указатель и ничего больше менять не можем.

В первом и втором варианте вполне можно обойтись использованием ссылок. Особой разницы нет. Для третьего ограничиться ссылкой получится не всегда. Например:

мужские шорты.

const char* const CONST_STR = "Hello, world!";

Строку в стиле C с помощью ссылки мы объявить не сможем. Нужен указатель. И он должен быть константным, поскольку изменение содержания строки запрещено и приведет к неопределенному поведению. А второй const здесь не помешает, чтобы получить жесткую привязку указателя к заданному значению и запретить случайные присвоения:

// Ничего нельзя:
// CONST_STR[ 0 ] = 'h';
// CONST_STR = "Good bye!";

Функции и const

Общее правило для применений const в функциях достаточно простое. Входные параметры лучше объявлять с ключевым словом const. Однако примитивные типы int, double, char и т.д. являются исключениями из этого правила. Это объясняется тем, что примитивные типы эффективнее передавать по значению, а не по ссылке или указателю. Причиной этому служит то, что большинство примитивных типов меньше по размеру, чем адрес в памяти, который нужно было бы передать в противном случае. Кроме того, передача по значению упрощает работу оптимизатору, поскольку выполняется прямое копирование, а не косвенные операции в адресном пространстве.

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

struct MyStruct {
    int x;
};

void myFunction( const MyStruct& myStruct ) {
    int x = myStruct.x; // можем читать
    // myStruct.x = 1; но не можем менять значение
}

Каноничным примером на этот случай является конструктор копирования:

class MyClass {
public:
    MyClass( const MyClass& other );
    // MyClass( MyClass other ); нельзя
};

Если бы мы попытались передать в конструктор копирования не ссылку, а значение, то для инициализации этого значения нам пришлось бы вызвать конструктор копирования, который мы и хотим реализовать.

Еще const можно использовать для объявления константных функций-членов классов:

class MyClass {
public:
    void set( int x );
    int get() const;
};

У класса в примере две функции: set() и get(). Первая предназначена для установки значения, а вторая для его получения. Ключевое слово const в этом случае позволяет нам явно об этом сообщить. Причем, эта информация будет полезна и компилятору, и тем, кто будет работать с нашим классом. Ведь они будут знать, что константные функции-члены не меняют состояние класса. Можно сравнить это с флагом read-only. Вот что будет, если передать константную ссылку на объект класса MyClass в функцию:

void myFunction( const MyClass& myClass ) {
    // myClass.set( 1 ); нельзя ничего менять
    int x = myClass.get(); // а вот читать пожалуйста
}

То есть объявив функцию-член get(), как константную, мы пояснили компилятору, что она не меняет состояние объекта и предназначена только для чтения. Если бы мы забыли про const, то в функции myFunction() мы бы ничего не смогли сделать с экземплярами класса MyClass, а компилятор бы выдавал ошибки при попытке вызова его функций-членов. Но если бы оказалось, что нам и правда нужно менять состояние объекта, то ключевое слово const из сигнатуры функции пришлось бы убрать. А по принятым соглашениям ссылку имело бы смысл заменить на указатель:

void myFunction( MyClass* myClass ) {
    myClass->set( 1 );
    int x = myClass->get();
}

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

class MyClass {
public:
    MyClass() : m_x( 0 ) {
    }

    void set( int x ) {
        std::lock_guard< std::mutex > lock( m_mutex );
        m_x = x;
    }

    int get() const {
        // Не будет работать, потому что меняется одно из полей класса:
        // std::lock_guard< std::mutex > lock( m_mutex );
        return m_x;
    }

private:
    int m_x;
    std::mutex m_mutex;
};

И что же делать? - Для этого в C++ предусмотрено ключевое слово mutable. Если мы объявим поле мьютекса, как mutable, то укажем компилятору, что состояние объекта может меняться даже в константных функциях-членах:

class MyClass {
public:
    MyClass() : m_x( 0 ) {
    }

    void set( int x ) {
        std::lock_guard< std::mutex > lock( m_mutex );
        m_x = x;
    }

    int get() const {
        std::lock_guard< std::mutex > lock( m_mutex ); // теперь все работает
        return m_x;
    }

private:
    int m_x;
    mutable std::mutex m_mutex;
};

Заключение

Вот мы и разобрались с ключевым словом const в С++. Оно имеет довольно много значений и позволяет улучшить надежность и читаемость кода, если правильно им пользоваться. Поэтому я нахожу довольно странным, что в большинстве других языков программирования, которые мне известны, хоть и есть некие аналоги const, но имеют в них ограниченную область применения. Более того, я считаю, что в C++ реализована одна из лучших возможных схем управления константами.

Похожие публикации

Комментарии

Добрый день.

Статья замечательная, НО…

Почему последний пример сделан с использованием Qt?

Речь же идёт о C++ и в течение всей статьи не было и намёка на Qt, также как и в заголовке статьи и в тегах к статье.

Смысл передан, но огорошивает применение в коде фреймворка Qt в качестве примера к статье, которая построена вокруг STL.

Было бы лучше переделать последний пример.

Legotckoi:

Добрый день.

Статья замечательная, НО…

Почему последний пример сделан с использованием Qt?

Речь же идёт о C++ и в течение всей статьи не было и намёка на Qt, также как и в заголовке статьи и в тегах к статье.

Смысл передан, но огорошивает применение в коде фреймворка Qt в качестве примера к статье, которая построена вокруг STL.

Было бы лучше переделать последний пример.

Здравствуйте. Спасибо за совет. Пример в ближайшее время поправлю.