С помощью ключевого слова const в C++ можно объявлять не только константы. Оно имеет много значений. И не лишним будем понимать каждое из них. Это позволяет писать более надежный код, указывая компилятору на ограничения, которые он должен проверять. Таким образом, любая попытка неправильного использования переменной или функции, объявленной с использованием const, будет выявлена на самых ранних этапах.
РЕКОМЕНДУЕМ: Способы повышения продуктивности для программиста
Переменные и const
Наиболее простой и интуитивно понятный вариант использования const заключается в объявлении константных значений:
1 2 |
const int CONST_VAL = 0; // CONST_VAL = 1; приводит к ошибке на этапе компиляции |
Похожего результата можно добиться с помощью макросов:
1 |
#define CONST_VAL 0 |
Но вариант с макросом не столь хорош, поскольку не дает возможности указать тип переменной. Из-за возможных проблем с типизацией лучше пользоваться константами, а не макросами. Более того, макросов лучше и вовсе избегать. В C++ реализована прекрасная поддержка шаблонных функций и классов, которые в большинстве случаев представляют более надежную альтернативу. Однако стоит признать, что бывают случаи, когда макросы оказываются полезными. Но сейчас речь не о них.
Таким образом, если мы объявили переменную с const, то значение должно быть присвоено сразу. Потом уже ничего не сделать. А что, если нам надо объявить константный вектор? В C++11 для этого предусмотрена специальная конструкция:
1 |
const std::vector< int > CONST_VECTOR = { 1, 2, 3, 4 }; |
А что, если мы по какой-то причине не может пользоваться C++11? И в этом случае можно легко объявить константный вектор:
1 2 3 4 5 6 7 8 9 10 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 и вместо константы использовать саму функцию. Но это уже дело вкуса:
1 2 3 4 5 6 7 8 9 10 11 |
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; } |
Константная ссылка объявляется схожим образом:
1 2 3 |
int x = 0; const int& xRef = x; // то же самое: int const& xRef = x; // xRef = 1; приводит к ошибке на этапе компиляции |
Суть константных ссылок заключается в том, что мы получаем доступ к значению переменной, на которую сделана ссылка, но изменить ее не имеем права. Основное назначение константных ссылок заключается в передаче входных параметров функциям. Но об этом немного позже.
Для указателей существует три варианта использования const:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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. Он равносилен случаю константной ссылки. То есть один раз объявили указатель и ничего больше менять не можем.
В первом и втором варианте вполне можно обойтись использованием ссылок. Особой разницы нет. Для третьего ограничиться ссылкой получится не всегда. Например:
1 |
const char* const CONST_STR = "Hello, world!"; |
Строку в стиле C с помощью ссылки мы объявить не сможем. Нужен указатель. И он должен быть константным, поскольку изменение содержания строки запрещено и приведет к неопределенному поведению. А второй const здесь не помешает, чтобы получить жесткую привязку указателя к заданному значению и запретить случайные присвоения:
1 2 3 |
// Ничего нельзя: // CONST_STR[ 0 ] = 'h'; // CONST_STR = "Good bye!"; |
Функции и const
Общее правило для применений const в функциях достаточно простое. Входные параметры лучше объявлять с ключевым словом const. Однако примитивные типы int, double, char и т.д. являются исключениями из этого правила. Это объясняется тем, что примитивные типы эффективнее передавать по значению, а не по ссылке или указателю. Причиной этому служит то, что большинство примитивных типов меньше по размеру, чем адрес в памяти, который нужно было бы передать в противном случае. Кроме того, передача по значению упрощает работу оптимизатору, поскольку выполняется прямое копирование, а не косвенные операции в адресном пространстве.
РЕКОМЕНДУЕМ: Десять советов по созданию гибкого программного кода
Если же вы хотите передать в функцию объект структуры или класс, то используйте константную ссылку:
1 2 3 4 5 6 7 8 |
struct MyStruct { int x; }; void myFunction( const MyStruct& myStruct ) { int x = myStruct.x; // можем читать // myStruct.x = 1; но не можем менять значение } |
Каноничным примером на этот случай является конструктор копирования:
1 2 3 4 5 |
class MyClass { public: MyClass( const MyClass& other ); // MyClass( MyClass other ); нельзя }; |
Если бы мы попытались передать в конструктор копирования не ссылку, а значение, то для инициализации этого значения нам пришлось бы вызвать конструктор копирования, который мы и хотим реализовать.
Еще const можно использовать для объявления константных функций-членов классов:
1 2 3 4 5 |
class MyClass { public: void set( int x ); int get() const; }; |
У класса в примере две функции: set() и get(). Первая предназначена для установки значения, а вторая для его получения. Ключевое слово const в этом случае позволяет нам явно об этом сообщить. Причем, эта информация будет полезна и компилятору, и тем, кто будет работать с нашим классом. Ведь они будут знать, что константные функции-члены не меняют состояние класса. Можно сравнить это с флагом read-only. Вот что будет, если передать константную ссылку на объект класса MyClass в функцию:
1 2 3 4 |
void myFunction( const MyClass& myClass ) { // myClass.set( 1 ); нельзя ничего менять int x = myClass.get(); // а вот читать пожалуйста } |
То есть объявив функцию-член get(), как константную, мы пояснили компилятору, что она не меняет состояние объекта и предназначена только для чтения. Если бы мы забыли про const, то в функции myFunction() мы бы ничего не смогли сделать с экземплярами класса MyClass, а компилятор бы выдавал ошибки при попытке вызова его функций-членов. Но если бы оказалось, что нам и правда нужно менять состояние объекта, то ключевое слово const из сигнатуры функции пришлось бы убрать. А по принятым соглашениям ссылку имело бы смысл заменить на указатель:
1 2 3 4 |
void myFunction( MyClass* myClass ) { myClass->set( 1 ); int x = myClass->get(); } |
Но тут есть один тонкий момент. Иногда бывает полезно инкапсулировать информацию о том, что на самом деле внутреннее состояние класса меняется, но все равно объявить функцию-член константной. Например, в многопоточной среде мы можем использовать мьютексы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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, то укажем компилятору, что состояние объекта может меняться даже в константных функциях-членах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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++ реализована одна из лучших возможных схем управления константами.
РЕКОМЕНДУЕМ: Основные структуры данных в C++