C ++ 11およびC ++ 14のConstexpr指定子

C ++ 11の新機能の1つにconstexpr



。 これを使用して、コンパイル段階で計算される変数、関数、さらにはオブジェクトを作成できます。 以前は、このような目的でテンプレートを使用する必要があったため、これは便利です。 しかし、すべてがそれほど単純ではありません。 constexpr



あまり慣れていない人は、コンパイル段階での計算に問題はないという印象を受けるかもしれません。 ただし、 constexpr



式には厳しい制限があります。



最初の部分ではconstexpr



について、C ++ 14標準でどのような変更が行われるかについてconstexpr



、2番目の部分ではconstexpr



の使用例をconstexpr



ます。文字列内の数式の結果を考慮するライブラリです。

これを使用して、次のコードを記述できます。

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





そして、分数の形式での回答は、コンパイル段階で受信されます。

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 _ _ ()





関数の前にC ++ 11で追加されたconstexpr



は、コンパイル段階でパラメータ値を計算できる場合、返される値もコンパイル段階で計算する必要があることを意味します。 コンパイル段階で少なくとも1つのパラメーターの値が不明な場合、関数はランタイムで起動されます(コンパイルエラーは出力されません)。



constexpr変数



constexpr = 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はcv指定子ではないため、const_castを使用してconstexpr指定子を追加することはできません( 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はサポートしません(ただし、CTPはVisual Studio“ 14”でサポートを追加しました)。



制限事項



これがどれほど便利かを理解したので、ハチミツの樽の中の軟膏にハエを追加できます。 そして、かなり大きなスプーン。



constexpr



変数の制約から始めましょう。 constexpr変数の型は、リテラル型、つまり次のいずれかでなければなりません。



constexpr



-variableは、次の条件を満たす必要があります。



珍しいことは何もないようです。 主な制限はconstexpr関数に課せられます:



* C ++ 14では、 void



もリテラル型になります。




constexpr



は、 return



節と1つの新しい節の追加を除いて、関数と同じ制限に従います。

すべての非静的クラスメンバーと基底クラスのメンバーは、何らかの方法で初期化する必要があります(宣言時に初期化リストまたはクラスメンバーの初期化を使用するコンストラクターで)。それらに割り当てられる式には、リテラルまたはconstexpr



-variablesおよびconstexpr



constexpr



のみを含める必要があります。



関数内の変数を初期化したりif-else



ループやif-else



作成することはできません。 一方で、これらの制限は、コンパイラーがコンパイル中に何らかの形でプログラムの実行をモニターする必要があるという事実によるものです(再帰はループよりも割り込みが容易です)。 一方、複雑な関数の作成には問題があります。



もちろん、すべて同じで、これらの可能性はすべて実現できます。 ループの代わりに再帰を使用し、 if-else



代わりに、「 ? :



? :



"、そして、変数を作成する代わりに、関数の値を使用します。



これはすべて、関数型プログラミングに非常に似ています。 関数型プログラミング言語では、原則として、変数は開始できず、ループもありません。 実際、関数を呼び出すことができ、関数へのポインターを使用して高次の関数を実装することもできます(残念ながら、constexpr構造では匿名関数(ラムダ)は使用できません)。 また、すべてのconstexpr関数は純粋な関数です(パラメーターにのみ依存し、結果のみを返します)。 constexpr



アルゴリズムを作成するには、少なくとも関数型プログラミングの初期知識が必要です。



しかし、ここでC ++には構文に大きな問題があります。匿名関数は使用できず、関数のすべてのアクションは1つの長い式であり、「 ? :



? :



»コードはまったく読めなくなります。 また、これらすべてには、数百行を占める可能性のある不可解なエラーメッセージが伴います。



しかし、問題はそれだけでは終わりません。 後でよく使用するconstexpr



関数を作成する場合、読み取り可能なエラーを返すと便利です。 ここでは、 static_assert



がこれにちょうど適切であると誤って想定されます。 ただし、関数パラメータはconstexpr



にできないため、 static_assert



は使用できません。これは、コンパイル時にパラメータ値が認識されることが保証されない理由です。

エラーを表示する方法は? 私が見つけた多かれ少なかれ通常の方法は、例外をスローすることです:

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





コンパイル中の関数呼び出しの場合、constructexpr関数にthrow構文を含めることができないというエラーが表示され、実行時に関数が例外をスローします。

間違いを見つけるのは難しいでしょうが、少なくとも何か。

gcc 4.8.2のエラー例
Main.cpp:16:24: 'MathCpp :: operator ""のconstexpr展開で(((const char *) "(67 + 987 ^(7-3 * 2))*(34-123)+ 17 ^ 2/0 +(-1) ")、37ul) '

MathCpp.h:115:28: 'MathCppのconstexpr展開::: solve(str、((size_t)size))'

MathCpp.h:120:103: 'MathCpp :: get_addsub(MathCpp :: SMathData(str、((int)size)、0))'のconstexpr拡張

MathCpp.h:209:89: 'MathCpp :: const getr 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: 'MathCpp :: get_muldiv(data.MathCpp :: SMathData :: create((((int)data.MathCpp :: SMathData :: start)+ 1)))のconstexpr拡張

MathCpp.h:181:83: 'MathCpp :: _ get_muldiv(data.MathCpp :: SMathData :: create((((int)MathCpp :: get_pow(data).MathCpp :: SMathValue :: end)+のconstexpr拡張で1))、MathCpp :: get_pow(data).MathCpp :: SMathValue :: value) '

MathCpp.h:38:111:エラー:式 '<throw-expression>'は定数式ではありません

#define math_assert(condition、description)((condition)?INVALID_VALUE:(throw std :: logic_error(description)、INVALID_VALUE))

^

MathCpp.h:195:15:注:マクロ「math_assert」の展開中

? math_assert(false、「ゼロ除算」)



このエラー出力方法はまだ言語標準に準拠していません;コンパイラがconstexpr



関数でconstexpr



使用できないというエラーを常にスローすることを禁止するものは何もありません。 GCC 4.8.2ではこれは機能しますが、Visual Studio "14"ではCTP C ++コンパイラはもうありません。



その結果、書くのが難しく、デバッグするのが難しく、そのような構造を理解するのが難しいです。

しかし、すべてがそれほど悪くはありません。C++ 14では、非常に多くの制限が削除されます。



C ++ 14での変更



既に述べたように、新しい標準では、 void



もリテラル型になり、たとえば、パラメーター値の正当性をチェックする関数を作成できるようになりました。



2番目の小さな変更点は、 constexpr



クラスのメンバー関数が定数ではなくなったことです。

C ++ 11では、次の行は同等でしたが、C ++ 14ではこれは当てはまりません。

 class car { constexpr int foo (int a); // C++11:     const, C++14 -   constexpr int foo (int a) const; };
      
      





これの説明はここで見つけることができます



そして最後に、主な変更により、 constexpr



関数およびコンストラクターでほぼすべての構成を使用できるようになりました。

constexpr



関数の本体には、 constexpr



を除く任意の構成体を含めることができます。





そしてconstexpr



本体は、より忠実な条件を満たしているはずです。





その結果、C ++ 14をサポートするコンパイラが登場した後、通常の関数とほとんど変わらないconstexpr関数を記述できるようになります。 それまでの間、非常にわかりにくいコードを作成する必要があります。



C ++ 11でconstexprを使用する例



constexprの使用例として、文字列にある数式の結果を読み取るライブラリを提供します。



そのため、次のようなコードを記述できるようにします。

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





別の新しいC ++ 11機能がここで使用されます:カスタムリテラル。 この場合、結果の値がconstexpr



変数に割り当てられている場合でも、コンパイル段階で関数が呼び出されることが保証されているため、これらは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) { return solve (str, size); }
      
      





次のマクロがアサートとして使用されます。

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





ライブラリは、加算、減算、乗算、除算、レイズができます。また、括弧のサポートもあります。 これは、再帰降下を使用して実装されます。



オペレーターの優先順位は次のとおりです(高いものから低いものへ)。

  1. 加算と減算
  2. 乗算と除算
  3. べき乗


数値、度、合計などを読み取る関数は、1つのパラメーター( SMathData



構造体)を取ります。 文字列、サイズ、 start



変数を保存します-読み取りを開始する場所:

 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



構造体を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つの機能(メイン1つと補助2つ)があります。

 //   (  ). 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); }
      
      





これは非常に複雑な設計です。 get_number



は、現在のインデックスが有効であることを確認し、 get_number



を呼び出して、最初の反復として数値の末尾を渡します(数値は右から左に読み取られます)。



ブラケットを使用する:

 // 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); }
      
      





現在のインデックスに番号がある場合、関数はget_number



呼び出し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



)を呼び出し、 get_pow



に、 get_pow



の範囲のvalueと等しい新しい値を渡します。



文字列"25"



get_pow



呼び出された場合に正しく処理されることが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) { return (y == 0) ? value : (x * _math_pow (x, y - 1, value)); }
      
      







製品と部門は1つの機能で処理されます。

 //      . 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_muldiv



および_getmuldiv



それぞれget_muldiv



および_getmuldiv



と同様に_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<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; }
      
      





最後にできることは、分子と分母を別々の変数として保存する数値のクラスを使用することです。 特別なものはありません。すべての関数とコンストラクターにconstexpr



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 << (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; }
      
      







その後、再帰降下のコードをわずかに変更し、必要な結果を取得する必要があります。

constexpr-functionsで再帰降下を記述するのに約1日かかりましたが、通常の再帰降下は1時間で問題なく書き込まれます。問題は、括弧との混同、デバッグの複雑さ、理解できないエラー、そして考えられないアーキテクチャ(今は、再帰的な降下であっても、すべてを注意深く検討する必要があります)です。



このライブラリのリポジトリはこちらです:https : //bitbucket.org/jjeka/mathcpp



欠点や質問がある場合は、書いてください!

PS C ++ 11/14専用のハブを作成するときが来たと思います。



All Articles