В чем разница между constexpr и const?
Заголовок: В чём разница между constexpr
и const
в C++?
Тело вопроса:
Я пытаюсь разобраться в различиях между constexpr
и const
в C++. У меня есть несколько вопросов по этой теме:
- Когда я могу использовать только одно из этих ключевых слов?
- В каких ситуациях я могу использовать оба, и как правильно выбрать между ними?
Буду признателен за пояснения и примеры!
5 ответ(ов)
Основное значение и синтаксис
Оба ключевых слова могут использоваться в объявлениях объектов, а также функций. Основное различие в применении к объектам следующее:
const
объявляет объект как константный. Это подразумевает гарантию того, что после инициализации значение этого объекта не изменится, и компилятор может использовать этот факт для оптимизаций. Это также помогает предотвратить написание кода, который изменяет объекты, не предназначенные для изменения после инициализации.constexpr
объявляет объект как подходящий для использования в том, что Стандарт называет константными выражениями. Однако следует отметить, чтоconstexpr
не является единственным способом сделать это.
При применении к функциям основное различие следующее:
const
может использоваться только для нестатических методов членов, а не для функций в общем. Это дает гарантию, что метод не изменяет ни одно из нестатических членов данных (за исключением изменяемых членов данных, которые можно изменять в любом случае).constexpr
может использоваться как с методами членов, так и с не-методами, а также с конструкторами. Оно объявляет функцию как подходящую для использования в константных выражениях. Компилятор примет её только в том случае, если функция соответствует определенным критериям (7.1.5/3,4), наиболее важным из которых является:- Тело функции должно быть не виртуальным и очень простым: помимо typedef и статических утверждений, разрешен только один оператор
return
. В случае конструктора разрешены только список инициализации, typedef и статические утверждения. (= default
и= delete
также разрешены.) - Начиная с C++14, правила стали более гибкими, и вот что стало возможным внутри
constexpr
функции: объявлениеasm
, операторgoto
, оператор с меткой, отличной отcase
иdefault
, блокtry
, определение переменной не-литерального типа, определение переменной с постоянной или поточной продолжительностью хранения, определение переменной, для которой инициализация не выполняется. - Аргументы и возвращаемый тип должны быть литеральными типами (то есть, в общем, очень простыми типами, обычно скалярами или агрегатами).
- Тело функции должно быть не виртуальным и очень простым: помимо typedef и статических утверждений, разрешен только один оператор
Константные выражения
Как было сказано выше, constexpr
объявляет как объекты, так и функции, как подходящие для использования в константных выражениях. Константное выражение — это больше, чем просто константа:
Его можно использовать в местах, где требуется оценка на этапе компиляции, например, в параметрах шаблонов и спецификаторах размера массивов:
template<int N> class fixed_size_list { /*...*/ }; fixed_size_list<X> mylist; // X должно быть целочисленным константным выражением int numbers[X]; // X должно быть целочисленным константным выражением
Но следует отметить:
Объявление чего-то как
constexpr
не гарантирует, что это будет оцениваться на этапе компиляции. Оно может быть использовано для этого, но может быть использовано и в других местах, которые оцениваются на этапе выполнения.Объект может быть пригодным к использованию в константных выражениях без объявления как
constexpr
. Пример:int main() { const int N = 3; int numbers[N] = {1, 2, 3}; // N — константное выражение }
Это возможно, потому что
N
, будучи константным и инициализированным во время объявления литералом, удовлетворяет критериям для константного выражения, даже если он не объявлен какconstexpr
.
Так когда же мне действительно нужно использовать constexpr
?
Объект, как
N
выше, может быть использован как константное выражение без объявленияconstexpr
. Это справедливо для всех объектов, которые:const
и- являются целочисленными или перечисляемыми типами и
- инициализированы во время объявления выражением, которое само является константным выражением.
Для того чтобы функция была пригодна к использованию в константных выражениях, она должна быть явно объявлена как
constexpr
; недостаточно просто соответствовать критериям для функций константных выражений. Пример:template<int N> class list { }; constexpr int sqr1(int arg) { return arg * arg; } int sqr2(int arg) { return arg * arg; } int main() { const int X = 2; list<sqr1(X)> mylist1; // OK: sqr1 является constexpr list<sqr2(X)> mylist2; // ошибочно: sqr2 не является constexpr }
Когда я могу/должен использовать и const
, и constexpr
вместе?
A. В объявлениях объектов. Это никогда не требуется, когда оба ключевых слова относятся к одному и тому же объекту, который объявляется. constexpr
подразумевает const
.
constexpr const int N = 5;
эквивалентно
constexpr int N = 5;
Однако следует отметить, что могут быть ситуации, когда ключевые слова относятся к разным частям объявления:
static constexpr int N = 3;
int main()
{
constexpr const int *NP = &N;
}
Здесь NP
объявляется как адрес константного выражения, т.е. указатель, который сам является константным выражением. (Это возможно, когда адрес генерируется путем применения оператора адреса к статическому/глобальному константному выражению.) Здесь оба constexpr
и const
необходимы: constexpr
всегда относится к выражению, которое объявляется (здесь NP
), в то время как const
относится к int
(он объявляет указатель на константный). Удаление const
сделает выражение нелегальным (потому что (a) указатель на неконстантный объект не может быть константным выражением, и (b) &N
фактически является указателем на константный объект).
B. В объявлениях методов членов. В C11 constexpr
подразумевает const
, в то время как в C14 и C17 это не так. Метод члена, объявленный в C11 как:
constexpr void f();
должен быть объявлен как
constexpr void f() const;
в C++14, чтобы все еще быть используемым как const
функция.
Оба ключевых слова const
и constexpr
могут быть применены к переменным и функциям. Хотя они выглядят схожими, на самом деле это совершенно разные концепции.
Оба const
и constexpr
означают, что их значения не могут быть изменены после инициализации. Например:
const int x1 = 10;
constexpr int x2 = 10;
x1 = 20; // ОШИБКА. Переменную 'x1' нельзя изменять.
x2 = 20; // ОШИБКА. Переменную 'x2' нельзя изменять.
Основное отличие между const
и constexpr
заключается во времени, когда их значения инициализируются (вычисляются). Значения переменных, объявленных с const
, могут быть вычислены как на этапе компиляции, так и на этапе выполнения, в то время как constexpr
всегда вычисляются на этапе компиляции. Пример:
int temp = rand(); // temp генерируется генератором случайных чисел во время выполнения.
const int x1 = 10; // ОК - известно на этапе компиляции.
const int x2 = temp; // ОК - известно только во время выполнения.
constexpr int x3 = 10; // ОК - известно на этапе компиляции.
constexpr int x4 = temp; // ОШИБКА. Компилятор не может определить значение переменной 'temp' на этапе компиляции, поэтому здесь нельзя использовать 'constexpr'.
Ключевое преимущество состоит в том, что константы времени компиляции могут использоваться везде, где необходимы константы времени компиляции. Например, в C++ нельзя объявлять массивы с переменными длины:
int temp = rand(); // temp генерируется генератором случайных чисел во время выполнения.
int array1[10]; // ОК.
int array2[temp]; // ОШИБКА.
Это означает, что:
const int size1 = 10; // ОК - значение известно на этапе компиляции.
const int size2 = temp; // ОК - значение известно только во время выполнения.
constexpr int size3 = 10; // ОК - значение известно на этапе компиляции.
int array3[size1]; // ОК - размер известен на этапе компиляции.
int array4[size2]; // ОШИБКА - размер известен только во время выполнения.
int array5[size3]; // ОК - размер известен на этапе компиляции.
Таким образом, const
переменные могут задавать как константы времени компиляции (например, size1
), которые могут быть использованы для указания размеров массивов, так и константы времени выполнения (например, size2
), которые известны только во время выполнения и не могут быть использованы для определения размеров массивов. С другой стороны, constexpr
всегда определяет константы времени компиляции, которые могут задавать размеры массивов.
Оба const
и constexpr
также могут применяться к функциям. Функция, помеченная как const
, должна быть методом класса, при этом использование ключевого слова const
означает, что метод не может изменять значения своих нестатических полей. Например:
class test
{
int x;
void function1()
{
x = 100; // ОК.
}
void function2() const
{
x = 100; // ОШИБКА. Константные методы не могут изменять значения полей объекта.
}
};
constexpr
— это другая концепция. Она помечает функцию (метод или не метод) как функцию, которая может быть вычислена на этапе компиляции при передаче ей значений, известных на этапе компиляции. Например, вы можете написать следующее:
constexpr int func_constexpr(int X, int Y)
{
return (X * Y);
}
int func(int X, int Y)
{
return (X * Y);
}
int array1[func_constexpr(10, 20)]; // ОК - func_constexpr() может быть вычислена на этапе компиляции.
int array2[func(10, 20)]; // ОШИБКА - func() не является функцией constexpr.
int array3[func_constexpr(10, rand())]; // ОШИБКА - хотя func_constexpr() является функцией 'constexpr', выражение 'func_constexpr(10, rand())' не может быть вычислено на этапе компиляции.
Кстати, функции constexpr
являются обычными функциями C++, которые могут быть вызваны даже если переданы не константные аргументы. Но в этом случае вы получите неконстантные значения.
int value1 = func_constexpr(10, rand()); // ОК. value1 - неконстантное значение, вычисленное во время выполнения.
constexpr int value2 = func_constexpr(10, rand()); // ОШИБКА. value2 - constexpr, и выражение func_constexpr(10, rand()) не может быть вычислено на этапе компиляции.
constexpr
также может применяться к методам классов, операторам и даже конструкторам. Например:
class test2
{
static constexpr int function(int value)
{
return (value + 1);
}
void f()
{
int x[function(10)];
}
};
Вот более «экстравагантный» пример:
class test3
{
public:
int value;
// constexpr const метод - не может изменять значения полей объекта и может быть вычислен на этапе компиляции.
constexpr int getvalue() const
{
return (value);
}
constexpr test3(int Value)
: value(Value)
{
}
};
constexpr test3 x(100); // ОК. Конструктор является constexpr.
int array[x.getvalue()]; // ОК. x.getvalue() является constexpr и может быть вычислено на этапе компиляции.
Согласно книге "Язык программирования C++ (4-е издание)" Бьярне Строупа:
const: это означает примерно «я обещаю не менять это значение» (§7.5). Он используется в первую очередь для определения интерфейсов, чтобы данные могли передаваться в функции без опасений, что они будут изменены. Компилятор обеспечивает выполнение этого обещания.
constexpr: это означает примерно «быть вычисленным на этапе компиляции» (§10.4). Он используется в первую очередь для задания констант.
Пример кода:
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 не будет изменять свой аргумент (§2.2.5)
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 можно использовать для неконстантных аргументов, но в этом случае результат не будет константным выражением. Мы разрешаем вызывать функцию constexpr с аргументами, которые не являются константными выражениями, в контекстах, где не требуются константные выражения, чтобы нам не приходилось определять по сути одну и ту же функцию дважды: один раз для константных выражений и один раз для переменных.
В некоторых местах требования языка требуют константные выражения (например, границы массивов (§2.2.5, §7.3), метки case (§2.2.4, §9.4.2), некоторые аргументы шаблонов (§25.2) и константы, объявленные с использованием constexpr). В других случаях оценка времени компиляции важна для производительности. Независимо от вопросов производительности, понятие неизменности (объект с неизменяемым состоянием) является важным аспектом проектирования (§10.4).
const int var
может быть задан динамически во время выполнения, и после того как он установлен на это значение, его больше нельзя изменить.
constexpr int var
не может быть задан динамически во время выполнения, а, скорее, во время компиляции. И как только он установлен на это значение, его также нельзя изменить.
Вот хороший пример:
int main(int argc, char* argv[]) {
const int p = argc;
// p = 69; // нельзя изменить p, потому что это const
// constexpr int q = argc; // делать это нельзя, потому что argc нельзя вычислить во время компиляции
constexpr int r = 2^3; // это работает!
// r = 42; // то же самое, что и const, его нельзя изменить
}
Этот фрагмент кода компилируется без ошибок, и я прокомментировал те строки, которые вызвали бы ошибки.
Ключевые моменты, на которые стоит обратить внимание, — это понятия время компиляции
и время выполнения
. В C++ были введены новые концепции, которые позволяют как можно больше **знать**
определенные вещи на этапе компиляции для оптимизации производительности во время выполнения.
Любая попытка объяснения, которая не включает эти два ключевых понятия, является заблуждением.
Как уже подметил @0x499602d2, const
только гарантирует, что значение не может быть изменено после инициализации, в то время как constexpr
(представленный в C++11) обеспечивает, что переменная является константой времени компиляции.
Рассмотрим следующий пример (из LearnCpp.com):
cout << "Введите ваш возраст: ";
int age;
cin >> age;
const int myAge{age}; // работает
constexpr int someAge{age}; // ошибка: age может быть разрешен только во время выполнения
В данном случае, переменная age
инициализируется значением, вводимым пользователем во время выполнения программы, поэтому компилятор не может гарантировать, что someAge
будет константой на этапе компиляции. const
позволяет сохранить значение age
, но не преобразует его в константу компиляции.
Что такое лямбда-выражение и когда его следует использовать?
Разница между const int*, const int * const и int * const?
push_back против emplace_back: в чем разница?
Что означает T&& (двойной амперсанд) в C++11?
Какова разница между 'typedef' и 'using'?