Constexpr c что это
Перейти к содержимому

Constexpr c что это

  • автор:

Спецификатор constexpr в C++: зачем он нужен и как работает

Новички могут путать спецификатор 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 
struct Number Number(const T& _val) :
value(_val) <>

T value;
>;

Если бы не поддержка этого шаблона класса, мы могли бы легко реализовать шаблон функции следующим образом.

template
T Square(const T& t) return t * t;
>

Однако этот шаблон функции не работает, когда ему передается объект типа Number .

int integer_num = 5;
float floating_num = 5.0;
bool boolean = true;
Number 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 Square(Number);
// ошибка компиляции, не найден operator*

Строка 9 не скомпилируется, потому что Number не реализует operator* .

Для решения этой проблемы нужно знать тип, переданный шаблону функции Square() , и добавить в него оператор проверки if-else , который будет вычисляться во время компиляции:

template
T Square(const T& t) if (std::is_arithmetic::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 std::enable_if::value, T>::type Square(const T& t) return t * t;
>

template
typename std::enable_if::value, T>::type Square(const T& t) return t.value * t.value;
>

Здесь указаны два шаблона функций: для арифметических и неарифметических типов. У std::enable_if с typedef :: type , если ему передается значение true , будет спецификатор доступа public. В противном случае члены с typedef с спецификатором доступа public будут отсутствовать.

При передаче Number в 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
T Square(const T& t) if constexpr (std::is_arithmetic::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++, о которых должен знать каждый разработчик

Спецификатор constexpr в C++11 и в C++14

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

В первой части будет рассказано про constexpr , о том, какие будут изменения в стандарте C++14, а во второй части будет пример использования constexpr : библиотека, которая считает результат математического выражения в строке.
С помощью нее можно будет написать следующий код:

constexpr auto x = "(4^2-9)/8+2/3"_solve; std::cout  

И ответ в виде дроби будет получен на этапе компиляции:
Answer is 37/24
Сразу предупреждаю, код этой библиотеки сложно понять.
Кому эта тема интересна, добро пожаловать под кат!

Что такое constexpr?

Сначала пара слов о том, что вообще такое спецификатор constexpr . Как уже было сказано, с помощью него можно производить какие-то операции на этапе компиляции. Выглядит это так:

constexpr int sum (int a, int b) < return a + b; >void func() < constexpr int c = sum (5, 12); // значение переменной будет посчитано на этапе компиляции >
constexpr-функция

constexpr возвращаемое_значение имя_функции (параметры)
Ключевое слово constexpr , добавленное в C++11, перед функцией означает, что если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции. Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime (а не будет выведена ошибка компиляции).

constexpr-переменная

constexpr тип = expression;
Ключевое слово в данном случае означает создание константы. Причем expression должно быть известно на этапе компиляции.

Рассмотрим такой пример:

int sum (int a, int b) < return a + b; >constexpr int new_sum (int a, int b) < return a + b; >void func() < constexpr int a1 = new_sum (5, 12); // ОК: constexpr-переменная constexpr int a2 = sum (5, 12); // ошибка: функция sum не является constexp-выражением int a3 = new_sum (5, 12); // ОК: функция будет вызвана на этапе компиляции int a4 = sum (5, 12); // ОК >

constexpr -переменная является константой ( const ), но константа не является constexpr-переменной.

В случае «утери» constexpr -спецификатора переменной вернуть обратно его уже не получится, даже если значение может посчитаться на этапе компиляции. constexpr -спецификатор нельзя добавить с помощью const_cast, так как constexpr не является cv-спецификатором (это const и volatile ). Такой код не заработает:

constexpr int inc (int a) < return a + 1; >void func() < int a = inc (3); constexpr int b = inc (a); // ошибка: a не является constexpr-выражением, из-за чего возвращаемое значение не имеет спецификатор constexpr >

Параметры функций не могут быть constexpr . То есть не получится создать исключительно constexpr -функцию, которая может работать только на этапе компиляции.

Также constexpr -функции могут работать с классами, это будет рассмотрено позже.

GCC, начиная с версии 4.4, поддерживает constexpr -функции, Clang также поддерживает с версии 2.9, а Visual Studio 2013 не поддерживает (но в Visual Studio «14» CTP наконец добавили поддержку).

Ограничения

Теперь, когда вы поняли, как это все удобно, можно добавить ложку дегтя в бочку меда. Причем довольно большую ложку.

  • Скалярный тип
  • Указатель
  • Массив скалярных типов
  • Класс, который удовлетворяет следующим условиям:
    • Имеет деструктор по умолчанию
    • Все нестатические члены класса должны быть литеральными типами
    • Класс должен иметь хотя бы один constexpr -конструктор (но не конструктор копирования и перемещения) или не иметь конструкторов вовсе
    • Ее тип должен быть литеральным
    • Ей должно быть сразу присвоено значение или вызван constexpr -конструктор
    • Параметры конструктора или присвоенное значение могут содержать только литералы или constexpr -переменные и constexpr -функции
    • Она не может быть виртуальной ( virtual )
    • Она должна возвращать литеральный тип ( void вернуть нельзя*)
    • Все параметры должны иметь литеральный тип
    • Тело функции должно содержать только следующее:
      • static_assert
      • typedef или using , которые объявляют все типы, кроме классов и перечислений ( enum )
      • using для указания видимости имен или пространств имен ( namespace )
      • Ровно один return , который может содержать только литералы или constexpr -переменные и constexpr -функции

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

      Получается, что в функциях нельзя инициализировать переменные, создавать циклы и конструкции if-else . С одной стороны, эти ограничения сделаны из-за того, что компилятору нужно хоть как-то отслеживать выполнение программы во время компиляции (рекурсию проще прерывать, чем циклы). С другой — писать сложные функции становится проблематично.

      Конечно, все равно все эти возможности можно реализовать. Вместо циклов использовать рекурсию, вместо конструкции if-else — оператор « ? : », а вместо создания переменных использовать значения функции.

      Все это сильно напоминает функциональное программирование. В функциональных языках программирования, как правило, также нельзя заводить переменные и отсутствуют циклы. Действительно, функции вызывать можно, функции высших порядков тоже можно реализовать, используя указатели на функции (к сожалению, анонимные функции (лямбды) нельзя использовать в constexpr -конструкциях). Также все constexpr-функции являются чистыми функциями (зависят только от своих параметров и возвращают только свой результат). Чтобы писать constexpr -алгоритмы, нужно иметь хотя бы начальные знания функционального программирования.

      Но тут у C++ большие проблемы с синтаксисом: анонимные функции нельзя использовать, все действия функции являются одним длинным выражением, а с добавлением оператора « ? : » код вовсе становится нечитабельным. Также все это сопровождается непонятными сообщениями об ошибке, которые могут занимать сотни строк.

      Но на этом проблемы не заканчиваются. Когда пишешь какую-то constexpr -функцию, которую потом будут часто использовать, хорошо бы возвращать читабельную ошибку. Тут можно ошибочно предположить, что static_assert как раз для этого подходит. Но static_assert использовать не получится, так как параметры функций не могут быть constexpr , из-за чего значения параметров не гарантированно будут известны на этапе компиляции.
      Как же выводить ошибки? Единственный более-менее нормальный способ, который я нашел, заключается в выбрасывании исключения:

      constexpr int div (int x, int y) < return (y == 0) ? throw std::logic_error ("x can't be zero") : (y / x); >

      В случае вызова функции во время компиляции мы увидим ошибку, что конструкция throw не может находиться в constexpr-функции, а в runtime функция выбросит исключение.
      Ошибку сложно будет найти, но хоть что-то.

      Пример ошибки в gcc 4.8.2

      Main.cpp:16:24: in constexpr expansion of ‘MathCpp::operator"" _solve(((const char*)"(67+987^(7-3*2))*(34-123)+17^2/0+(-1)"), 37ul)’
      MathCpp.h:115:28: in constexpr expansion of ‘MathCpp::solve(str, ((size_t)size))’
      MathCpp.h:120:103: in constexpr expansion of ‘MathCpp::get_addsub(MathCpp::SMathData(str, ((int)size), 0))’
      MathCpp.h:209:89: in constexpr expansion of ‘MathCpp::_get_addsub(data.MathCpp::SMathData::create((((int)MathCpp::get_muldiv(data).MathCpp::SMathValue::end) + 1)), MathCpp::get_muldiv(data).MathCpp::SMathValue::value)’
      MathCpp.h:217:50: in constexpr expansion of ‘MathCpp::get_muldiv(data.MathCpp::SMathData::create((((int)data.MathCpp::SMathData::start) + 1)))’
      MathCpp.h:181:83: in constexpr expansion of ‘MathCpp::_get_muldiv(data.MathCpp::SMathData::create((((int)MathCpp::get_pow(data).MathCpp::SMathValue::end) + 1)), MathCpp::get_pow(data).MathCpp::SMathValue::value)’
      MathCpp.h:38:111: error: expression ‘’ is not a constant-expression
      #define math_assert(condition,description) ((condition)? INVALID_VALUE: (throw std::logic_error (description), INVALID_VALUE))
      ^
      MathCpp.h:195:15: note: in expansion of macro ‘math_assert’
      ? math_assert (false, «Division by zero»)

      Такой способ вывода ошибки еще не соответствует стандарту языка, ничего не запрещает компилятору всегда выдавать ошибку о том, что нельзя использовать throw в constexpr -функции. В GCC 4.8.2 это работает, а в Visual Studio «14» CTP C++ compiler — уже нет.

      В итоге сложно писать, сложно отлаживать, сложно понимать такие конструкции.
      Но все не так плохо, в C++14 очень многие ограничения уберут.

      Изменения в C++14

      Как уже было сказано, в новом стандарте void также будет литеральным типом, и теперь можно будет создавать функции, которые, например, будут проверять значения параметров на правильность.

      Второе незначительное изменение заключается в том, что теперь constexpr функции-члены класса не являются константными.
      В C++11 следующие строчки были равносильными, а в С++14 это уже не так:

      class car < constexpr int foo (int a); // C++11: функция неявно получает спецификатор const, C++14 - не получает constexpr int foo (int a) const; >; 

      Объяснение этому можно найти тут.

      • Ассемблерных вставок
      • Ключевого слова goto
      • Определения переменных нелитерального типа или static и thread_safe -переменных. Все переменные должны инициализироваться при определении.
      • Он должен соответствовать всем условиям constexpr -функции
      • Все его нестатические члены должны иметь литеральный тип
      • Аналогичное условие про то, что все нестатические члены класса должны каким-либо способом инициализироваться
      • Появилась возможность использовать union 'ы, но с некоторыми ограничениями

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

      Пример использования constexpr на C++11

      В качестве примера использования constexpr будет приведена библиотека, которая будет считать результат математического выражения, находящегося в строке.

      Итак, мы хотим, чтобы можно было писать такой код:

      constexpr auto n = "(67+987^(7-3*2))*(34-123)+17^2+(-1)"_solve; std::cout  

      Тут используется еще одна новая возможность C++11: пользовательские литералы. В данном случае они хороши тем, что функция гарантированно будет вызвана на этапе компиляции, даже если получившееся значение будет присвоено не constexpr -переменной.

      Объявляется пользовательский литерал таким образом:

      constexpr int operator "" _solve (const char* str, const size_t size); constexpr int solve (const char* str, const size_t size); constexpr int operator "" _solve (const char* str, const size_t size)

      В качестве ассерта будет использоваться следующий макрос:

      #define math_assert(condition,description) ((condition) ? 0 : (throw std::logic_error (description), 0)) 

      Библиотека может складывать, вычитать, умножать, делить, возводить в степень, также есть поддержка скобок. Реализовано это будет с помощью рекурсивного спуска.

      1. Сложение и вычитание
      2. Умножение и деление
      3. Возведение в целую степень
      struct SMathData < constexpr SMathData (const char* _str, const int _size, const int _start) : str (_str), size (_size), start (_start) <>constexpr SMathData create (const int _start) const < return SMathData (str, size, _start); >constexpr char char_start() const < return char_at (start); >constexpr char char_at (const int pos) const < return (pos >= 0 && pos < size) ? str[pos] : ((pos == size) ? 0 : (math_assert (false, "Internal error: out of bounds"), 0)); >const char* str; const int size; const int start; >; 

      А возвращать эти функции будут структуру SMathValue . В ней хранятся посчитанное значение и end — переменная, в которую записан конец числа, суммы, произведения или чего-то еще:

      struct SMathValue < constexpr SMathValue (const int _value, const int _end) : value (_value), end (_end) <>constexpr SMathValue add_end (int dend) const < return SMathValue (value, end + dend); >const int value; const int end; >; 

      Для считывания числа будут 3 функции (одна основная и две вспомогательных):

      // Считывает число (поддерживается унарный минус). constexpr SMathValue get_number (const SMathData data); // Рекурсивная функция считывания числа с его конца (без унарного минуса и проверок). // Если positive == true, то функция вернет положительное число, а если false - то отрицательное. i - индекс цифры в строке. constexpr SMathValue _get_number (const SMathData data, const int i, const bool positive); // Возвращает индекс последней цифры числа в строке (start - начало числа). constexpr int _get_number_end (const SMathData data); constexpr SMathValue get_number (const SMathData data) < return (data.char_start() == '-') ? (math_assert (data.char_at (data.start + 1) >= '0' && data.char_at (data.start + 1) = '0' && data.char_start() constexpr SMathValue _get_number (const SMathData data, const int i, const bool positive) < return (i >= data.start) ? SMathValue (_get_number (data, i - 1, positive).value * 10 + (positive ? 1 : -1) * (data.char_at (i) - '0'), i) : SMathValue (0, data.start - 1); > constexpr int _get_number_end (const SMathData data) < return (data.char_start() >= '0' && data.char_start()

      Вот такая запутанная конструкция получается. get_number проверяет, что на текущем индексе действительно число и вызывает _get_number , передавая в качестве первой итерации конец числа (число читается справа налево).

      Работа со скобками:

      // get branum - сокращение от get bracket or number. constexpr SMathValue get_branum (const SMathData data); constexpr SMathValue get_branum (const SMathData data)

      Если на текущем индексе число, то функция вызывает get_number , в противном случае функция считает выражение в скобках.

      Дальше идет функция возведения в степень:

      // Возвращает значение после возведения в степень. constexpr SMathValue get_pow (const SMathData data); // Вспомогательная функция. Тут предполагается, что start ссылается на следующий символ после конца первого числа (или выражения), // то есть на символ '^', если он присутствует. value - значение первого числа (или выражения). constexpr SMathValue _get_pow (const SMathData data, const int value); constexpr SMathValue get_pow (const SMathData data) < return _get_pow (data.create (get_branum (data).end + 1), get_branum (data).value); >constexpr SMathValue _get_pow (const SMathData data, const int value) < return (data.char_start() == '^') ? _get_pow (data.create // start (get_branum (data.create (data.start + 1)).end + 1), // value math_pow (value, get_branum (data.create (data.start + 1)).value)) : SMathValue (value, data.start - 1); >

      В функции _get_pow проверяется, что текущий символ '^' . Если это так, то функция вызывает сама себя (точнее get_pow ), передав туда новое значение, равное value в степени прочитанное_значение.

      Получается, что строка "25" правильно обработается, если для нее вызвать get_pow . Так как в этом случае просто прочитается число, после чего оно вернется.
      math_pow — простая constexpr -функция возведения в целую степень.

      Реализация math_pow

      constexpr int math_pow (const int x, const int y); constexpr int _math_pow (const int x, const int y, const int value); constexpr int math_pow (const int x, const int y) < return math_assert (y >= 0, "Power can't be negative"), _math_pow (x, y.to_int(), 1); > constexpr int _math_pow (const int x, const int y, const int value)

      Произведение и деление обрабатываются в одной функции:

      // Возвращает результат после умножения и деления. constexpr SMathValue get_muldiv (const SMathData data); // Вспомогательная функция. Аналогична _get_pow. constexpr SMathValue _get_muldiv (const SMathData data, const int value); constexpr SMathValue get_muldiv (const SMathData data) < return _get_muldiv (data.create (get_pow (data).end + 1), get_pow (data).value); >constexpr SMathValue _get_muldiv (const SMathData data, const int value) < return (data.char_start() == '*') ? _get_muldiv (data.create // start (get_pow (data.create (data.start + 1)).end + 1), // value value * get_pow (data.create (data.start + 1)).value) : ((data.char_start() == '/') ? (get_pow (data.create (data.start + 1)).value == 0) ? math_assert (false, "Division by zero") : _get_muldiv (data.create // start (get_pow (data.create (data.start + 1)).end + 1), // value value / get_pow (data.create (data.start + 1)).value) : SMathValue (value, data.start - 1)); >

      Довольно сложно понять эту конструкцию, писать ее также затруднительно. Тут идет проверка, является ли текущий символ '*' , если это так, то функция вызывает сама себя, перемножая value на прочитанное число (или выражение). В случае с '/' функция ведет себя аналогично, только перед этим идет проверка на то, что знаменатель не равен нулю. Если текущий символ не является '*' или '/' , то просто возвращается значение.

      Аналогично происходит с суммой и разностью:

      Реализация get_addsub

      constexpr SMathValue get_addsub (const SMathData data); constexpr SMathValue _get_addsub (const SMathData data, const CMathVariable value); constexpr SMathValue get_addsub (const SMathData data) < return _get_addsub (data.create (get_muldiv (data).end + 1), get_muldiv (data).value); >constexpr SMathValue _get_addsub (const SMathData data, const CMathVariable value) < return (data.char_start() == '+') ? _get_addsub (data.create // start (get_muldiv (data.create (data.start + 1)).end + 1), // value value + get_muldiv (data.create (data.start + 1)).value) : ((data.char_start() == '-') ? _get_addsub (data.create // start (get_muldiv (data.create (data.start + 1)).end + 1), // value value - get_muldiv (data.create (data.start + 1)).value) : SMathValue (value, data.start - 1)); >

      Работа функций get_addsub и _get_addsub аналогична работе функций get_muldiv и _getmuldiv соответственно.

      И наконец, осталось реализовать функцию solve :

      constexpr CMathVariable solve (const char* str, const size_t size); // get_value проверяет, что была прочитана вся строка // (то есть, что value.end == size), и возвращает результат. constexpr int get_value (const int size, const SMathValue value); constexpr int solve (const char* str, const size_t size) < return get_value (static_cast(size), get_addsub (SMathData (str, static_cast (size), 0))); > constexpr int get_value (const int size, const SMathValue value)

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

      Собственный класс чисел

      class CMathVariable < private: int64_t numerator_; uint64_t denominator_; constexpr CMathVariable (int64_t numerator, uint64_t denominator); constexpr int64_t sign_ (int64_t a) const; constexpr uint64_t gcd_ (uint64_t a, uint64_t b) const; constexpr CMathVariable reduce_() const; public: constexpr explicit CMathVariable (int number); constexpr CMathVariable operator + (const CMathVariable& n) const; constexpr CMathVariable operator - (const CMathVariable& n) const; constexpr CMathVariable operator * (const CMathVariable& n) const; constexpr CMathVariable operator / (const CMathVariable& n) const; constexpr int64_t numerator() const; constexpr uint64_t denominator() const; constexpr bool is_plus_inf() const; constexpr bool is_menus_inf() const; constexpr bool is_nan() const; constexpr bool is_inf() const; constexpr bool is_usual() const; constexpr bool is_integer() const; constexpr int to_int() const; constexpr int force_to_int() const; constexpr double to_double() const; friend constexpr CMathVariable operator - (const CMathVariable& n); friend constexpr CMathVariable operator + (const CMathVariable& n); friend std::ostream& operator ; constexpr CMathVariable operator - (const CMathVariable& n); constexpr CMathVariable operator + (const CMathVariable& n); std::ostream& operator constexpr CMathVariable::CMathVariable (int64_t numerator, uint64_t denominator) : numerator_ (numerator), denominator_ (denominator) < >constexpr int64_t CMathVariable::sign_ (int64_t a) const < return (a >0) - (a < 0); >constexpr uint64_t CMathVariable::gcd_ (uint64_t a, uint64_t b) const < return (b == 0) ? a : gcd_ (b, a % b); >constexpr CMathVariable CMathVariable::reduce_() const < return (numerator_ == 0) ? CMathVariable (0, sign_ (denominator_)) : ((denominator_ == 0) ? CMathVariable (sign_ (numerator_), 0) : CMathVariable (numerator_ / gcd_ (static_cast(std::abs (numerator_)), denominator_), denominator_ / gcd_ (static_cast (std::abs (numerator_)), denominator_))); > constexpr int64_t CMathVariable::numerator() const < return numerator_; >constexpr uint64_t CMathVariable::denominator() const < return denominator_; >constexpr bool CMathVariable::is_plus_inf() const < return denominator_ == 0 && numerator_ >0; > constexpr bool CMathVariable::is_menus_inf() const < return denominator_ == 0 && numerator_ < 0; >constexpr bool CMathVariable::is_nan() const < return denominator_ == 0 && numerator_ == 0; >constexpr bool CMathVariable::is_inf() const < return denominator_ == 0 && numerator_ != 0; >constexpr bool CMathVariable::is_usual() const < return denominator_ != 0; >constexpr bool CMathVariable::is_integer() const < return denominator_ == 1; >constexpr int CMathVariable::to_int() const < return static_cast(numerator_ / denominator_); > constexpr int CMathVariable::force_to_int() const < return (!(denominator_ == 1 && static_cast(numerator_) == numerator_) ? (throw std::logic_error ("CMathVariable can't be represented by int"), 0) : 0), to_int(); > constexpr double CMathVariable::to_double() const < return static_cast(numerator_) / denominator_; > constexpr CMathVariable CMathVariable::operator + (const CMathVariable& n) const < return CMathVariable ( static_cast(n.denominator_ / gcd_ (denominator_, n.denominator_)) * numerator_ + static_cast (denominator_ / gcd_ (denominator_, n.denominator_)) * n.numerator_, denominator_ / gcd_ (denominator_, n.denominator_) * n.denominator_).reduce_(); > constexpr CMathVariable CMathVariable::operator - (const CMathVariable& n) const < return CMathVariable ( static_cast(n.denominator_ / gcd_ (denominator_, n.denominator_)) * numerator_ - static_cast (denominator_ / gcd_ (denominator_, n.denominator_)) * n.numerator_, denominator_ / gcd_ (denominator_, n.denominator_) * n.denominator_).reduce_(); > constexpr CMathVariable CMathVariable::operator * (const CMathVariable& n) const < return CMathVariable ( numerator_ * n.numerator_, denominator_ * n.denominator_).reduce_(); >constexpr CMathVariable CMathVariable::operator / (const CMathVariable& n) const < return CMathVariable ( numerator_ * static_cast(n.denominator_) * (n.numerator_ ? sign_ (n.numerator_) : 1), denominator_ * static_cast (std::abs (n.numerator_))).reduce_(); > constexpr CMathVariable operator + (const CMathVariable& n) < return n; >constexpr CMathVariable operator - (const CMathVariable& n) < return CMathVariable (-n.numerator_, n.denominator_); >std::ostream& operator 
      

      После этого надо немного изменить код рекурсивного спуска и в итоге получить требуемое.
      Написание рекурсивного спуска на constexpr-функциях заняло где-то день, хотя обычный рекурсивный спуск без проблем пишется за час. Проблемы были с путаницей со скобками, со сложностью отладки, с непонятными ошибками, с непродуманностью архитектуры (да, теперь даже для рекурсивного спуска надо тщательно все продумывать).

      Репозиторий с этой библиотекой находится тут: https://bitbucket.org/jjeka/mathcpp

      Если есть какие-то недочеты или вопросы, пишите!
      P.S. Считаю, что уже пора бы создавать хаб, посвященный C++11/14.

      constexpr specifier (начиная с C++11)

      constexpr specifier заявляет, что можно оценить значение функции или переменной во время компиляции. Затем такие переменные и функции можно использовать там, где разрешено только время компиляции constant expressions (при условии, что соответствующие аргументы функции given).

      constexpr specifier, используемый в объявлении объекта или функции-члена, отличной от static (до тех пор, пока C++14) не подразумевает const. constexpr specifier, используемый в функции или данных static . member(since Объявление C++17) подразумевает встраивание. Если какое-либо объявление функции или шаблона функции имеет constexpr specifier, то каждое объявление должен содержать этот спецификатор.

      constexpr variable

      Переменная constexpr должна удовлетворять следующим требованиям:

      • его тип должен быть LiteralType .
      • он должен быть немедленно инициализирован
      • full-expression его инициализации, включая все неявные преобразования, вызовы конструкторов и т. д., должен быть constant expression
      • он должен иметь постоянное разрушение, Т14153Т либо:
        • это не тип класса и не его (возможно, многомерный) массив, или
        • это тип класса или (возможно, многомерный) его массив, этот тип класса имеет деструктор constexpr, и для гипотетического выражения e , единственным эффектом которого является уничтожение объекта, e будет основным константным выражением, если считается, что время жизни объекта и его неизменяемых подобъектов (но не его изменяемых подобъектов) начинается в пределах e .

        Если переменная constexpr не является translation-unit-local , ее не следует инициализировать, чтобы она указывала, или ссылалась, или имела (возможно, рекурсивный) подобъект, который указывает или ссылается на локальную единицу перевода сущность, которую можно использовать в константных выражениях. Такая инициализация запрещена в module interface unit (за пределами его фрагмента модуля private, если таковой имеется) или в разделе модуля и не рекомендуется в любом другом контексте.

        constexpr function

        Функция constexpr должна удовлетворять следующим требованиям:

        • это не должно быть virtual
        • это не должно быть function-try-block
        • это не должен быть coroutine
        • для конструктора и destructor(since C++20), класс не должен иметь виртуальных базовых классов
        • его возвращаемое значение (если есть) и каждый из его параметров должен иметь LiteralType
        • существует по крайней мере один набор значений аргументов, так что вызов функции может быть оцениваемым подвыражением core constant expression (для конструкторов используется в constant initializer sufficient)(since C++14). Для нарушения этого пункта диагностика не требуется.
        • тело функции должно быть либо удалено, либо установлено по умолчанию, либо содержать только следующее:
          • null statements (простая точка с запятой)
          • static_assert declarations
          • Объявления typedef и объявления alias , которые не определяют классы или перечисления
          • using declarations
          • using directives
          • если функция не является конструктором, ровно один оператор return
          • тело функции не должно содержать:
            • заявление goto
            • оператор с label , отличным от регистра и значения по умолчанию
            • a try-block
            • asm declaration
            • определение переменной, для которой no initialization is performed
            • определение переменной нелитерального типа
            • определение переменной static или потока storage duration
            constexpr constructor

            Конструктор constexpr, тело функции которого не =delete; , должен удовлетворять следующим дополнительным требованиям:

            • для конструктора class or struct каждый подобъект базового класса и каждый элемент данных non-variant , отличный от static, должны быть инициализированы. Если класс является union-like class , для каждого из его непустых анонимных членов объединения должен быть инициализирован ровно один вариантный член.
            • для конструктора непустого union должен быть инициализирован ровно один элемент данных, отличный от static.
            • каждый конструктор, выбранный для инициализации элементов данных, отличных от static, и базового класса, должен быть конструктором constexpr.
            constexpr destructor

            Деструкторы не могут быть constexpr, но trivial destructor можно неявно вызывать в константных выражениях.

            Деструктор constexpr, тело функции которого не =delete; , должен удовлетворять следующему дополнительному требованию:

            • каждый деструктор, используемый для уничтожения членов данных, отличных от static, и базового класса, должен быть деструктором constexpr.

            Для шаблонов функций constexpr и функций-членов шаблонов классов constexpr по крайней мере одна специализация должна удовлетворять вышеупомянутым требованиям. Другие специализации по-прежнему считаются constexpr, хотя вызов такой функции не может появляться в постоянном выражении. Если никакая специализация шаблона не удовлетворяет требованиям к функции constexpr, если рассматривать ее как функцию, не являющуюся шаблоном, то шаблон имеет неправильный формат, нет диагностики required.(until C++23).

            Notes

            Поскольку оператор noexcept всегда возвращает true для константного выражения, его можно использовать для проверки того, принимает ли конкретный вызов функции constexpr ветвь константного выражения:

            constexpr int f(); constexpr bool b1 = noexcept(f()); // false, неопределенная функция constexpr constexpr int f() < return 0; > constexpr bool b2 = noexcept(f()); // true, f() — константное выражение

            Можно написать функцию constexpr, вызов которой никогда не сможет удовлетворить требования базового константного выражения:

            void f(int& i) // не функция constexpr < i = 0; > constexpr void g(int& i) // правильный формат с C++23 < f(i); // безоговорочно вызывает f, не может быть константным выражением >

            Конструкторы Constexpr разрешены для классов, не являющихся литеральными типами. Например, конструктором std::unique_ptr по умолчанию является constexpr, что позволяет использовать constant initialization .

            Ссылочные переменные могут быть объявлены constexpr (их инициализаторы должны быть reference constant expressions ):

            static constexpr int const& x = 42; // ссылка constexpr на объект типа const int // (объект имеет срок хранения static // в связи с продлением срока службы по ссылке static)

            Несмотря на то, что блоки try и встроенная сборка разрешены в функциях constexpr, создание исключений или выполнение сборки по-прежнему запрещено в константном выражении.

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

            Feature-test macro Value Std Comment
            __cpp_constexpr 200704L (C++11) constexpr
            201304L (C++14) Л135048Л, Н253530Н
            201603L (C++17) Constexpr lambda
            201907L (C++20) Тривиальные default initialization и asm-declaration в функциях constexpr
            202002L (C++20) Изменение активного члена союза в постоянной оценке
            202110L (C++23) Не- literal переменные, метки и операторы goto в функциях constexpr
            202207L (C++23) Ослабление некоторых ограничений constexpr
            202211L (C++23) Разрешение переменных staticconstexpr в функциях constexpr
            __cpp_constexpr_in_decltype 201711L (C++11)
            (DR)
            Генерация определений функций и переменных при needed for constant evaluation
            __cpp_constexpr_dynamic_alloc 201907L (C++20) Операции для длительности динамического хранения в функциях constexpr

            Keywords

            Example

            Определение функции constexpr C++11, которая вычисляет факториалы и литеральный тип, расширяющий строковые литералы:

            #include #include // Функции constexpr C++11 используют рекурсию, а не итерацию constexpr int factorial(int n) < return n 1 ? 1 : (n * factorial(n - 1)); > // Функции constexpr C++14 могут использовать локальные переменные и циклы #if __cplusplus >= 201402L constexpr int factorial_cxx14(int n) < int res = 1; while (n > 1) res *= n--; return res; > #endif // C++14 // литеральный класс class conststr < const char* p; std::size_t sz; public: templatesize_t N> constexpr conststr(const char(&a)[N]): p(a), sz(N - 1) > // функции constexpr сигнализируют об ошибках, генерируя исключения // в C++11, это надо делать из условного оператора ?: constexpr char operator[](std::size_t n) const < return n < sz ? p[n] : throw std::out_of_range(""); > constexpr std::size_t size() const < return sz; > >; // Функции constexpr C++11 должны были помещать все в один оператор return // (C++14 не имеет этого требования) constexpr std::size_t countlower(conststr s, std::size_t n = 0, std::size_t c = 0) < return n == s.size() ? c : 'a' <= s[n] && s[n] 'z' ? countlower(s, n + 1, c + 1) : countlower(s, n + 1, c); > // выходная функция, требующая константы времени компиляции, для тестирования templateint n> struct constN < constN() < std::cout '\n'; > >; int main() < std::cout "4! hljs-built_in">factorial(4)> out1; // вычисляется во время компиляции volatile int k = 8; // запрещаем оптимизацию с использованием volatile std::cout << k "! hljs-built_in">factorial(k) '\n'; // вычисляется во время выполнения std::cout "the number of lowercase letters in \"Hello, world!\" is "; constNcountlower("Hello, world!")> out2; // неявно преобразуется в conststr constexpr int a[12] = 0, 1, 2, 3, 4, 5, 6, 7, 8>; constexpr int length_a = sizeof(a)/sizeof(int); // std::size(a) в C++17, // std::ssize(a) в C++20 std::cout "array of length " << length_a " has elements: "; for (int i = 0; i < length_a; ++i) std::cout << a[i] " "; >
            4! = 24 8! = 40320 the number of lowercase letters in "Hello, world!" is 9 array of length 12 has elements: 0 1 2 3 4 5 6 7 8 0 0 0

            Defect reports

            Следующие отчеты о дефектах, изменяющих поведение, были применены задним числом к ранее опубликованным стандартам C++.

            DR Applied to Поведение после публикации Correct behavior
            CWG 1712 C++14 шаблон переменной constexpr требовался, чтобы иметь все
            его объявления содержат constexpr specifier
            (это избыточно, потому что не может быть более одного
            объявление шаблона переменной с constexpr specifier)
            больше не требуется
            CWG 1911 C++11 Конструкторы constexpr для нелитеральных типов были запрещены. разрешено в константной инициализации
            CWG 2004 C++11 copy/move союза с изменяемым элементом
            было разрешено в постоянном выражении
            изменяемые варианты дисквалифицируют
            implicit copy/move
            CWG 2163 C++14 метки были разрешены в функциях constexpr
            даже несмотря на то, что готы запрещены
            этикетки также запрещены
            CWG 2268 C++11 copy/move объединения с изменяемым членом было
            запрещено постановлением К135048К
            разрешено, если объект создан
            в постоянном выражении

            See also

            constant expression определяет expression , который можно оценить во время компиляции
            consteval specifier(C++20) указывает, что функция является immediate function , то есть каждый вызов функции должен быть в постоянном вычислении
            constinit specifier(C++20) утверждает, что переменная имеет инициализацию static, i.e. zero initialization и constant initialization
            C documentation для constexpr

            Разница между const и constexpr

            const int x - переменная x не должна меняться в процессе работы программы, а constexpr int x - оно еще и должна получить свое значение во время компиляции.

            int n; cin >> n; const int x = n*n; 

            Отработает, x получит свое значение, которое нельзя будет менять - но получит во время работы программы.

            Если написать constexpr - не скомпилируется, ибо во время компиляции x не известно.

            Соответственно этому - используйте то, что более подходит для ваших целей. Где можно - лучше constexpr , где нет - просто const .

            Отслеживать
            ответ дан 12 ноя 2016 в 16:13
            218k 15 15 золотых знаков 117 117 серебряных знаков 229 229 бронзовых знаков

            Самый простой пример. Данная программа компилируется

            #include struct A < constexpr static double x = 10.0; >; int main()

            А данная программа нет.

            #include struct A < const static double x = 10.0; >; int main()

            Существенное значение также имеет место, когда этот спецификатор, constexpr , используется для функций.

            Как вы знаете, только функции-члены класса могут иметь квалификатор const , который имеет отношение к объекту, для которого вызывается данная функция-член класса.

            Обычные функции не могут быть константными.

            Спецификатор constexpr введен для того, чтобы заставить компилятор на этапе кмпиляции создавать объекты и использовать их как константы времени компиляции.

            Например, известно, что стандарт C++ для задания размерности массива требует константное выражение. Используя спецификатор constexpr вы можете задавать размер массива, используя некоторые функции. Например,

            #include struct A < constexpr A( bool b ) : n( b ? 5 : 10 ) <>size_t n; >; int main()

            Вывод программы на консоль

            0 1 2 3 4 0 1 2 3 4 5 6 7 8 9 

            Использование constexpr позволяет заниматься метапрограммированием на этапе компиляции, как альтернативная возможность для шаблонного метапрограммирования.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *