C++ поддерживает две нотации неизменности:
- const - которая подразумевает, что значение не будет изменяться. В первую очередь это используется для спецификации интерфейсов, для данных которые передаются в функции и методы так, чтобы не опасаться, что они будут изменены. Компилятор отслеживает наличие спецификатора const;
- constexp - который подразумевает вычисление константы во время компиляции. Используется для размещения данных в памяти, где они не будут повреждены, а также для улучшения производительности.
Например:
const int dmv = 17; // константа с именем dmv int var = 17; // переменная var не является константой constexpr double max1 = 1.4∗square(dmv); // OK, так как square(17) является константым выражением constexpr double max2 = 1.4∗square(var); // Ошибка, так как var не является константой const double max3 = 1.4∗square(var); // OK, поскольку выражение может быть вычислено в рантайме double sum(const vector<double>&); // sum не будет модифицировать его аргументы vector<double> v {1.2, 3.4, 4.5}; // v не является константой const double s1 = sum(v); // OK, будет вычислено в рантайме constexpr double s2 = sum(v); // Ошибка, sum(v) не является константным выражением.
Для того, чтобы функция использовалась в константном выражении, то есть, вычислялась компилятором, необходимо определить её со спецификатором constexpr .
constexpr double square(double x) { return x∗x; }
constexpr функция должна быть достаточно простой, чтобы вычисляться компилятором, а также возвращать вычисленное значение. constexpr функции могут вызываться неконстантыми аргументами в контексте которых не требуются константные выражения.
const
Объекты со спецификатором const не могут быть изменены, а также должны быть инициализированы.
const int model = 90; // model является константой const int v[] = { 1, 2, 3, 4 }; // v[i] является константой const int x; // Ошибка: значение не инициализировано
Поскольку объекты со спецификаторов const не могут быть изменены, то следующий код будет ошибочным:
void f() { model = 200; // Ошибка: model является константой v[2] = 3; // Ошибка: v[i] является константой }
Обратите внимание, что const изменяет тип объекта, а не указание того, как должна быть назначена переменная. const ограничивает способы работы с объектом.
void g(const X∗ p) { // Не можем изменить p здесь } void h() { X val; // Но можем модифицировать значение здесь g(&val); // ... }
При использовании указателя задействуются два объекта: сам указатель и объект, на который указывает. Префиксное' объявление указателя с const делает константным объект, а не указатель. Чтобы объявить как const сам указатель, а не объект, на который он указывает, необходимо поместить const после символа указателя. Например:
void f1(char∗ p) { char s[] = "Gorm"; const char∗ pc = s; // указатель на константу pc[3] = 'g'; // Ошибка: значение объекта является константой pc = p; // OK char ∗const cp = s; // константный указатель cp[3] = 'a'; // OK cp = p; // Ошибка: cp является константой const char ∗const cpc = s; // константный указатель на константный объект cpc[3] = 'a'; // Ошибка: объект является константой cpc = p; // Ошибка: указатель является константой }
Местоположение const относительно базового типа не принципиально, поскольку не существует типа данных const*. Принципиальным является положение const относительно символа *. Поэтому возможны следующие записи:
char ∗const cp; // const указатель на char char const∗ pc; // указать на const char const char∗ pc2; // указатель на const char
Объект, который является константой при доступе через один указатель, может быть изменяемым при доступе иными способами. Это особенно полезно для аргументов функции. Объявляя аргумент указателя как const , функции запрещается изменять объект, на который указывает. Например:
const char∗ strchr(const char∗ p, char c); char∗ strchr(char∗ p, char c);
Первая версия используется для строк, элементы которых не должны быть изменены функцией и возвращает указатель на const, который не позволяет изменять результат. Вторая версия используется для изменяемых строк.
Вы можете назначить адрес неконстантной переменной указателю на константу, потому что это не может нанести никакого вреда. Однако адрес константы нельзя назначить неконстантному указателю, поскольку это позволит изменить значение объекта. Например:
void f4() { int a = 1; const int c = 2; const int∗ p1 = &c; // OK const int∗ p2 = &a; // OK int∗ p3 = &c; // Ошибка: инициализация int* с const int* ∗p3 = 7; // Попытка изменить значение c }
constexpr
Константное выражение является выражением, которое вычисляется во время компиляции. Константные выражения не могут использовать значения и переменные, которые не известны во время компиляции.
Существует множество причин, по которым кому-то может понадобиться именованная константа, а не буква или значение, хранящееся в переменной:
- Именованные константы упрощают понимание и поддержку кода.
- Переменная может быть изменена (поэтому мы должны быть более осторожными в наших рассуждениях, чем для константы).
- Язык требует постоянных выражений для размеров массивов, меток case и аргументов значений шаблона.
- Программисты встраиваемых систем любят помещать неизменяемые данные в постоянное запоминающее устройство. Потому что доступная только для чтения память дешевле, чем динамическая память (с точки зрения затрат и потребления энергии) и часто более многочисленная. Кроме того, данные в постоянной памяти защищены от большинства сбоев системы.
- Если инициализация выполняется во время компиляции, то в многопоточной программе системе не может быть никаких расхождений данных.
- Выполнение вычислений на этапе компиляции улучшает производительность программы.
Значение constexpr вычисляется во время выполнения компиляции, и если оно не может быть вычислено, то компилятор выдаст ошибку.
int x1 = 7; constexpr int x2 = 7; constexpr int x3 = x1; // Ошибка: инициализатор не является константным выражением constexpr int x4 = x2; // OK void f() { constexpr int y3 = x1; // Ошибка: инициализатор не является константным выражением constexpr int y4 = x2; // OK // ... }
Возможности константных выражений достаточно велики, поскольку имеется возможность использовать целочисленнные типы данных, данные с плавающей точкой, перечисления, а также операторы, которые не изменяют значения переменных (такие как +, ? и [] , но не = или ++ )
constexpr int isqrt_helper(int sq, int d, int a) { return sq <= a ? isqrt_helper(sq+d,d+2,a) : d; } constexpr int isqrt(int x) { return isqrt_helper(1,3,x)/2 − 1; } constexpr int s1 = isqrt(9); constexpr int s2 = isqrt(1234);
Отличное описание и примеры!