📜 ⬆️ ⬇️

The constexpr qualifier in C ++ 11 and C ++ 14

One of the new features of C ++ 11 is the constexpr . With it you can create variables, functions and even objects that will be calculated at the compilation stage. This is convenient, because earlier for such purposes it was necessary to use templates. But it's not so simple. Those who are not so familiar with constexpr may get the impression that now there will be no problems with calculations at the compilation stage. But constexpr expressions are severely constrained.

The first part will tell you about constexpr , what will be the changes in the C ++ 14 standard, and the second part will be an example of using constexpr : a library that considers the result of a mathematical expression in a string.
With it you can write the following code:
 constexpr auto x = "(4^2-9)/8+2/3"_solve; std::cout << "Answer is " << x; 

And the answer in the form of a fraction will be obtained at the compilation stage:
Answer is 37/24
Immediately I warn you, the code of this library is difficult to understand.
To whom this topic is interesting, welcome under the cat!

What is constexpr?


First, a constexpr words about what the constexpr . As already mentioned, it can be used to perform some operations at the compilation stage. It looks like this:
 constexpr int sum (int a, int b) { return a + b; } void func() { constexpr int c = sum (5, 12); //        } 

constexpr function


constexpr _ _ ()
The constexpr added in C ++ 11 before the function means that if the parameter values ​​can be calculated at the compilation stage, then the return value should also be counted at the compilation stage. If the value of at least one parameter is unknown at compile time, the function will be launched at runtime (and no compilation error will be displayed).

constexpr variable


constexpr = expression;
The key word in this case is the creation of a constant. And expression should be known at the compilation stage.
')
Consider this example:
 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 variable is a constant ( const ), but the constant is not a constexpr variable.

In the case of "loss" constexpr -specific variable to return it back will not work, even if the value can be considered at the compilation stage. constexpr -specifier cannot be added using const_cast, since constexpr is not a cv-specifier (this is const and volatile ). This code will not work:
 constexpr int inc (int a) { return a + 1; } void func() { int a = inc (3); constexpr int b = inc (a); // : a   constexpr-, -       constexpr } 


Function parameters cannot be constexpr . That is, it will not be possible to create an exclusively constexpr -function that can only work at the compilation stage.

Also, constexpr functions can work with classes, this will be discussed later.

GCC, starting with version 4.4, supports constexpr functions, Clang also supports version 2.9, and Visual Studio 2013 does not support (but in Visual Studio "14" CTP has finally added support).

Restrictions


Now that you understand how all this is convenient, you can add a spoonful of tar in a barrel of honey. And quite a big spoon.

Let's start with the constexpr variable constraints. The constexpr variable type must be a literal type, that is, one of the following:

constexpr variable must satisfy the following conditions:

There seems to be nothing unusual. The main restrictions are imposed on the constexpr-functions:

* With C ++ 14, void will also be a literal type.

The constexpr are subject to the same restrictions as the functions, with the exception of the clause about return and the addition of one new clause:
All nonstatic class members and base class members must be initialized in some way (in the constructor, using initialization lists or initializing class members when they are declared), and the expressions assigned to them must contain only literals or constexpr variables and constexpr -functions.

It turns out that in functions it is impossible to initialize variables, create loops and if-else . On the one hand, these restrictions are made due to the fact that the compiler needs to at least somehow monitor the execution of the program during compilation (recursion is easier to interrupt than cycles). On the other hand, writing complex functions becomes problematic.

Of course, all the same, all these possibilities can be realized. Instead of cycles, use recursion, instead of the if-else , the operator “ ? : ? : ", And instead of creating variables, use the function values.

All this strongly resembles functional programming. In functional programming languages, as a rule, it is also impossible to create variables and there are no cycles. Indeed, you can call functions, higher-order functions can also be implemented using function pointers (unfortunately, anonymous functions (lambdas) cannot be used in constexpr ). Also, all constexpr-functions are pure functions (depend only on their parameters and return only their result). To write constexpr algorithms, you must have at least a basic knowledge of functional programming.

But here C ++ has big problems with syntax: anonymous functions cannot be used, all functions of the function are one long expression, but with the addition of the operator “ ? : ? : ”The code is completely unreadable. Also, all this is accompanied by incomprehensible error messages that can take hundreds of lines.

But the problems do not end there. When you write some constexpr function, which will then be used frequently, it would be good to return a readable error. Here you can mistakenly assume that static_assert is suitable for this. But static_assert cannot be used, since the parameters of the functions cannot be constexpr , which is why the values ​​of the parameters are not guaranteed to be known at the compilation stage.
How to display errors? The only more or less normal way I found is to throw an exception:
 constexpr int div (int x, int y) { return (y == 0) ? throw std::logic_error ("x can't be zero") : (y / x); } 

In the case of a function call at compile time, we see an error that the throw construct cannot be in the constexpr function, and the function will throw an exception at runtime.
It will be difficult to find an error, but at least something.
Error example in 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 '<throw-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")

This way of outputting an error does not yet correspond to the standard of the language; nothing prevents the compiler from always giving an error stating that you cannot use throw in the constexpr functions. In GCC 4.8.2, this works, but in Visual Studio “14” the CTP C ++ compiler does not.

As a result, it is difficult to write, difficult to debug, difficult to understand such constructions.
But everything is not so bad, in C ++ 14 very many restrictions will be removed.

Changes in C ++ 14


As already mentioned, in the new standard, void will also be a literal type, and now it will be possible to create functions that, for example, will check the values ​​of parameters for correctness.

The second minor change is that now the constexpr class member functions are not constant.
In C ++ 11, the following lines were equivalent, but in C ++ 14, this is no longer the case:
 class car { constexpr int foo (int a); // C++11:     const, C++14 -   constexpr int foo (int a) const; }; 

An explanation for this can be found here .

Finally, a major change allows the use of almost any constructs in constexpr functions and constructors.
Now the body of constexpr -functions can contain any constructions, except:


And the body of the constexpr should now satisfy more loyal conditions:


As a result, after the appearance of compilers that support C ++ 14, it will be possible to write constexpr-functions that are almost no different from the usual ones. In the meantime, you have to write quite confusing code.

An example of using constexpr in C ++ 11


As an example of using constexpr, a library will be given that will read the result of a mathematical expression that is in a line.

So, we want to be able to write such code:
 constexpr auto n = "(67+987^(7-3*2))*(34-123)+17^2+(-1)"_solve; std::cout << "Answer is " << n; 

Another new feature of C ++ 11 is used here: custom literals. In this case, they are good because the function is guaranteed to be called at compile time, even if the resulting value is not assigned to a constexpr variable.

A custom literal is declared this way:
 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) { return solve (str, size); } 

The following macro will be used as an assertion:
 #define math_assert(condition,description) ((condition) ? 0 : (throw std::logic_error (description), 0)) 

The library can add, subtract, multiply, divide, raise, there is also support for brackets. It will be implemented using recursive descent.

The priorities of the operators will be as follows (from higher to lower):
  1. Addition and subtraction
  2. Multiplication and division
  3. Erection in the whole degree

The functions of reading the number, degree, sum, and so on will take one parameter: the structure of SMathData . A string is stored in it, its size and the start variable - where to start reading:
 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; }; 

And return these functions will be the structure of SMathValue . It stores the calculated value and end is a variable in which the end of the number, sum, product or something else is written:
 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; }; 


For reading the numbers there will be 3 functions (one main and two auxiliary):
 //   (  ). 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) <= '9', "Not a digit"), _get_number (data.create (data.start + 1), _get_number_end (data.create (data.start + 1)), false)) : (math_assert (data.char_start() >= '0' && data.char_start() <= '9', "Digit required"), _get_number (data, _get_number_end (data), true)); } 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() <= '9') ? _get_number_end (data.create (data.start + 1)) : (data.start - 1); } 

Here such confused construction turns out. get_number checks that the current index is actually a number and calls _get_number , passing the end of the number as the first iteration (the number is read from right to left).

Working with brackets:
 // get branum -   get bracket or number. constexpr SMathValue get_branum (const SMathData data); constexpr SMathValue get_branum (const SMathData data) { return (data.char_start() == '(') ? (math_assert (data.char_at (get_addsub (data.create (data.start + 1)).end + 1) == ')', "')' required"), get_addsub (data.create (data.start + 1)).add_end (1)) : get_number (data); } 

If the current index is a number, the function calls get_number ; otherwise, the function counts the expression in brackets.

Next comes the exponentiation function:
 //      . 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); } 

The _get_pow function checks that the current character is '^' . If this is the case, the function calls itself (or rather, get_pow ), passing there a new value equal to value in the degree read_value.

It turns out that the string "25" correctly processed if get_pow called for it. Since in this case the number is simply read, after which it will return.
math_pow is a simple constexpr function.
Implementation 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) { return (y == 0) ? value : (x * _math_pow (x, y - 1, value)); } 


Product and division are processed in one function:
 //      . 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)); } 

It is rather difficult to understand this construction, it is also difficult to write it. Here there is a check whether the current character is '*' , if so, then the function calls itself, multiplying value by the read number (or expression). In the case of '/' function behaves similarly, only before this there is a check that the denominator is not zero. If the current character is not '*' or '/' , then the value is simply returned.

Similarly, with the sum and difference:
Implementation 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)); } 

The functions get_addsub and _get_addsub similar to the functions get_muldiv and _getmuldiv respectively.

Finally, it remains to implement the solve function:
 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<int> (size), get_addsub (SMathData (str, static_cast<int> (size), 0))); } constexpr int get_value (const int size, const SMathValue value) { return math_assert (value.end + 1 == size, "Digit or operator required"), value.value; } 

And the last thing you can do: use your class of numbers, in which the numerator and denominator will be stored as separate variables. There is nothing special here, just all the functions and the constructor have the constexpr .
Own class of numbers
 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 << (std::ostream& os, const CMathVariable& var); }; constexpr CMathVariable operator - (const CMathVariable& n); constexpr CMathVariable operator + (const CMathVariable& n); std::ostream& operator << (std::ostream& os, const CMathVariable& var); constexpr CMathVariable::CMathVariable (int number) : numerator_ (number), denominator_ (1) { } 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<uint64_t> (std::abs (numerator_)), denominator_), denominator_ / gcd_ (static_cast<uint64_t> (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<int> (numerator_ / denominator_); } constexpr int CMathVariable::force_to_int() const { return (!(denominator_ == 1 && static_cast<int> (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<double> (numerator_) / denominator_; } constexpr CMathVariable CMathVariable::operator + (const CMathVariable& n) const { return CMathVariable ( static_cast<int64_t> (n.denominator_ / gcd_ (denominator_, n.denominator_)) * numerator_ + static_cast<int64_t> (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<int64_t> (n.denominator_ / gcd_ (denominator_, n.denominator_)) * numerator_ - static_cast<int64_t> (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<int64_t> (n.denominator_) * (n.numerator_ ? sign_ (n.numerator_) : 1), denominator_ * static_cast<uint64_t> (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 << (std::ostream& stream, const CMathVariable& var) { if (var.is_plus_inf()) stream << "+inf"; else if (var.is_menus_inf()) stream << "-inf"; else if (var.is_nan()) stream << "nan"; else if (var.denominator() == 1) stream << var.numerator(); else stream << var.numerator() << " / " << var.denominator(); return stream; } 


After that, you need to slightly change the code of the recursive descent and eventually get what you want.
Writing a recursive descent on constexpr-functions took about a day, although the usual recursive descent is written without problems in an hour. The problems were with a confusion with brackets, with the complexity of debugging, with incomprehensible errors, with an ill-conceived architecture (yes, now even for recursive descent, everything must be carefully thought out).

The repository with this library is here: https://bitbucket.org/jjeka/mathcpp

If you have any shortcomings or questions, write!
PS I believe that it is already time to create a hub dedicated to C ++ 11/14.

Source: https://habr.com/ru/post/228181/


All Articles