Новички могут путать спецификатор constexpr
, появившийся в C++ с версии 11, с похожим на него квалификатором const
.
constexpr
обозначает константное выражение и используется для определения переменной или функции в качестве константного выражения, которое может вычисляться на этапе компиляции. Это его главная особенность — код может быть выполнен до его запуска.
constexpr
можно использовать с переменными и функциями, в том числе конструкторами и операторами if
. Разберемся в этом подробнее.
Переменные
Квалификатор const
указывает компиляторам и другим программистам, что переменная доступна только для чтения. Любая попытка изменить ее приведет к ошибке компиляции.
В данном случае constexpr
похож на const
тем, что подразумевает const
. Его тоже нельзя изменить. Разница в том, что const
может быть вычислен как на стадии компиляции, так при выполнении программы, в зависимости от выбранного варианта инициализации.
int main() {
const int val = 1 + 2;
return 0;
}
В этом примере val
вычисляется во время компиляции. При выполнении кода val
всегда равно 3
. Однако в следующем примере val
вычисляется во время выполнения кода, поскольку включает вызов функции.
int Sum(const int a, const int b) {
return a + b;
}
int main(int argc, char **argv) {
const int val = Sum(1, 2);
return 0;
}
Поскольку первый пример вычисляется во время компиляции, мы можем заменить его на constexpr
.
int main() {
constexpr int val = 1 + 2;
return 0;
}
Но это не применимо ко второму примеру. Мы получим ошибку компиляции из-за невозможности вычисления на этом этапе.
error: call to non-‘constexpr’ function ‘int Sum(int, int)’
Чтобы устранить эту проблему в данном примере, можно преобразовать функцию в constexpr
.
constexpr int Sum(const int a, const int b) {
return a + b;
}
constexpr int main(int argc, char **argv) {
const int val = Sum(1, 2);
return 0;
}
Код выглядит так же, но теперь выполняется во время компиляции. При выполнении компилятор модифицирует код так:
int main(int argc, char **argv) {
int val = 3;
return 0;
}
Здесь не указаны const
и constexpr
, потому что после запуска программы они больше не используются. И const
, и constexpr
применяются только в процессе компиляции.
Функции и конструкторы
constexpr
можно также использовать с функциями и конструкторами. Как и в предыдущем примере, можно определить функцию или конструктор как функцию constexpr
.
Функции constexpr
отличаются гибкостью. Одна и та же функция может быть вычислена во время компиляции и выполнения. Все зависит от того, как они вызываются.
constexpr int Sum(const int a, const int b) {
return a + b;
}
int main(int argc, char **argv) {
constexpr int val = Sum(1, 2);
int var = 3;
int val2 = Sum(val, var);
return 0;
}
В этом примере есть функция constexpr
под названием Sum
, которая вызывается в строках 6 и 8. Строка 6 вычисляется при компиляции (как и в примере предыдущего раздела), а строка 8 — при выполнении, поскольку включает неконстантную переменную var
.
Очевидно, что функция constexpr
весьма полезна. Мы можем предоставить компилятору выбор: вычислять ее при компиляции или при выполнении. Для вычисления во время компиляции должен выполняться целый ряд условий (подробнее об этом по ссылке).
Операторы if
Начиная с C++17, можно определять if
-операторы constexpr
. Если вы не часто сталкиваетесь с общим кодом в повседневной работе, то, возможно, пользуетесь этой возможностью. Чтобы выяснить, зачем она необходима, нужно понимать концепцию SFINAE (Substitution Failure is not an Error, ошибка замены не является ошибкой) в метапрограммировании шаблона C++.
SFINAE и std::enable_if
При вызове функции компилятор должен выполнить проверку, чтобы узнать, какую из функций вызывать. Учитывайте перегрузку функций (function overloading), позволяющую давать нескольким функциям одинаковые имена. Говоря упрощенно, есть несколько шагов:
- Name lookup (поиск по имени);
- Template Argument Deduction (дедукция аргумента шаблона);
- Template Argument Substitution (замена аргумента шаблона);
- Overload Resolution (разрешение перегрузки).
В первых трех шагах создается набор функций перегрузки, которые будут использованы на последнем шаге. Overload Resolution выберет функцию с наиболее точно подходящими параметрами.
Алгоритм SFINAE проявляется на шаге 3 (Template Argument Substitution), когда функция-кандидат проваливает тест Substitution (замены). Ошибка компиляции не возникает, а функция просто удаляется из списка кандидатов, как в следующем примере.
Нам нужно написать общую функцию Square()
, которая может быть как арифметического, так и определяемого пользователем типа. Определяемый пользователем тип — это шаблон класса, показанный ниже.
template <typename T>
struct Number {
Number(const T& _val) :
value(_val) {}
T value;
};
Если бы не поддержка этого шаблона класса, мы могли бы легко реализовать шаблон функции следующим образом.
template<typename T>
T Square(const T& t) {
return t * t;
}
Однако этот шаблон функции не работает, когда ему передается объект типа Number<int>
.
int integer_num = 5;
float floating_num = 5.0;
bool boolean = true;
Number<int> number_int(5);
auto res = Square(integer_num); // вызов int Square(int);
auto res2 = Square(floating_num); // вызов float Square(float);
auto res3 = Square(boolean); // вызов bool Square(bool);
auto res4 = Square(number_int); // вызов Number<int> Square(Number<int>);
// ошибка компиляции, не найден operator*
Строка 9 не скомпилируется, потому что Number<int>
не реализует operator*
.
Для решения этой проблемы нужно знать тип, переданный шаблону функции Square()
, и добавить в него оператор проверки if-else
, который будет вычисляться во время компиляции:
template<typename T>
T Square(const T& t) {
if (std::is_arithmetic<T>::value) {
return t * t;
} else {
return t.value * t.value;
}
}
Но такое решение не работает: при вызове, например с int
, эта функция пытается найти int.value
, которого не существует. Чтобы увидеть это более четко, смотрим результат создания экземпляра шаблона.
int Square(const int& t) {
if (true) {
return t * t;
} else {
return t.value * t.value;
}
}
Теперь понятно, почему он не работает. Часть else
не удалена из функции, и мы получаем ошибку компиляции.
error: request for member ‘value’ in ‘t’, which is of non-class type ‘const int’
Чтобы решить эту проблему, нужны два шаблона функций, проверяющих, является ли передаваемый тип арифметическим.
template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type Square(const T& t) {
return t * t;
}
template<typename T>
typename std::enable_if<! std::is_arithmetic<T>::value, T>::type Square(const T& t) {
return t.value * t.value;
}
Здесь указаны два шаблона функций: для арифметических и неарифметических типов. У std::enable_if
с typedef :: type
, если ему передается значение true
, будет спецификатор доступа public. В противном случае члены с typedef с спецификатором доступа public будут отсутствовать.
При передаче Number<int>
в Square()
первый шаблон функции завершает замену шаблона с ошибкой, а второй — успешно. Ошибки для первой функции не возникает, она просто удаляется из списка функций-кандидатов. Затем компилятор выбирает вторую функцию.
С помощью двух и более шаблонов функций с std::enable_if
мы в некотором роде имитируем if-else
во время компиляции.
Как оператор constexpr if улучшает SFINAE
Взаимодействие SFINAE и std::enable_if
работает и часто используется, но не очень интуитивно понятно. Такой излишне подробный код и незнакомый синтаксис порой трудно читать.
Сделать его более читаемым, начиная с C++17, позволяют if
-операторы constexpr
. Можно использовать настоящий if-else
во время компиляции в одной функции, а не имитировать его, используя несколько функций с std::enable_if
. Ниже показана реализация с помощью if
-оператора constexpr
.
template<typename T>
T Square(const T& t) {
if constexpr (std::is_arithmetic<T>::value) {
return t * t;
} else {
return t.value * t.value;
}
}
В этом примере используется только один шаблон функции, который к тому же намного ближе к знакомому нам оператору if-else
. Этот способ работает, потому что компилятор берет только ветку с истинным условием (true
) и отбрасывает другие.
Выводы
- При использовании для переменных
constexpr
подразумеваетconst
. Главная особенность заключается в том, что переменныеconstexpr
вычисляются на этапе компиляции. - Спецификатор
constexpr
может использоваться с функциями и конструкторами. В отличие от функций, возвращающихconst
, использованиеconstexpr
в этом случае позволяет вычислять функции и конструкторы на этапе компиляции (если это возможно). - Чтобы улучшить читаемость кода,
constexpr
можно использовать для исполняемого во время компиляцииif-else
вместо обычной имитации с помощью SFINAE иstd::enable_if
.
Читайте также:
- Google Test: интеграция модульных тестов в C/C++ проекты
- Как работает программа «Hello World!»?
- Возможности C++, о которых должен знать каждый разработчик
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Debby Nirwan: C++ constexpr: What It Really Is?