C++の基礎 - constexpr

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

C++には、constexprという概念がある。

今まで、const修飾してきたものには2種類ある。

  • ROM化可能な値。
  • 実行時にしか決まらないが、一度初期化した後は二度と変更されない値。


C++ 11以降、前者はconstexprが受け持ち、後者はconstが受け持つことになった。
constexprは、constexprの制約を満たした変数の定義、関数と関数テンプレートの宣言、staticデータメンバの宣言に対して使用できる。

つまり、constはRAMにしか配置できない変数に対して使う型修飾子となった。
constexprは型修飾子ではなく、型指定子である。
型を修飾するものでなく、ROM化できる、または、ROM化できる可能性がある、という意味を持つ指定子である。

  • constexprが使用できない、または、使用すべきではない場合
    • 変数
      constでない変数
      クラスのメンバ変数
      標準入力などの非constexpr関数を使用して計算する値
      引数等のconstexprでない可能性がある値を使用して計算する値

    • 関数
      インライン化できない関数
      引数でもthisでもない非constexprな外側の変数を参照する操作を含む関数
      引数でもthisでもない外側に副作用を及ぼすような操作を含む関数

  • constexprを使用すべき場合
    • 上記以外全て



constexpr変数

constexpr変数は、#define等で作成するようなコンパイル時定数を作るためのキーワードである。

constとは、「この変数は変更しないため、変更しようとする場合はコンパイルエラーにする」という合図である。
constexprとは、「この変数の値はコンパイル時に確定するため、確定しない場合はコンパイルエラーにする」という合図である。

また、constexpr変数は、const変数としても扱われる。

コンパイラは、constexpr変数の値をコンパイル時に計算しようとする。
もし、計算できなければ、コンパイルエラーを出力する。

どの操作がコンパイル時に計算可能かは、関数にconstexprキーワードが付いているかどうかで判断される。(後述のセクションで記載する)

以下の例では、1行目でbufSizeを9に初期化している。
この演算はコンパイル時に行うため、constexpr変数を初期値9で宣言することにより、ROM上に配置される。

 constexpr unsigned int bufSize = sizeof("abc def¥n");
 static_assert(bufSize == 9, "bufSize is not 9");
 
 char buf[bufSize] = {0};
 fgets(buf, sizeof(buf), stdin);
 
 const int a = atoi(strtok(buf," ¥n"));
 const int b = atoi(strtok(nullptr," ¥n"));
 
 std::cout << (a + b) / 2 << std::endl;


以下の例では、列挙型Colorはint32_t型であり、3つの値をとる。
static宣言されたColor型の配列は、constexpr指定子が付加されて、ROM上に配置される。

 typedef enum Color : int32_t
 {
    Red = 0,
    Yellow,
    Blue
 } Color;
 
 constexpr static Color colors[] =
 { 
    Color::Red,
    Color::Yellow,
    Color::Blue 
 };


以下のサンプルコードでは、標準入力から受け取る操作がコンパイル時に行えないため、コンパイルエラーとなる。

 #include <iostream>
 
 // 標準入力からint型の値を受け取って返す(コンパイル時に値が定まらない)関数
 auto get_value_from_stdin()
 {
    int v;
    std::cin >> v;
    return v;
 }
 
 int main()
 {
    auto a = 1;            // 通常の変数
    const auto b = 2;      // Const
    constexpr auto c = 3;  // Constexpr
 
    a = 4;     // コンパイル可能    aは後から書き換えてよい
    // b = 5;  // コンパイルエラー  bはconstなので書き換えてはいけない
    // c = 6;  // コンパイルエラー  cはconstなので書き換えてはいけない
    std::cout << a << ", " << b << ", " << c << std::endl;
 
    auto d = get_value_from_stdin();               // コンパイル可能    dは実行時に受け取った値
    const auto e = get_value_from_stdin();         // コンパイル可能    eは実行時に受け取り、今後変更されない値
    // constexpr auto f = get_value_from_stdin();  // コンパイルエラー  fはコンパイル時に値が確定しなければならない
    std::cout << d << ", " << e << /* ", " << f << */ std::endl;
 
    return 0;
 }



constexpr関数

constexpr関数の基本

もし、コンパイル時において、関数の引数にconstexpr変数を使用する場合、戻り値でconstexpr変数を初期化する文はコンパイルできる。
もし、コンパイル時において、関数の引数にconstexprでない変数を使用する場合、戻り値でconstexpr変数を初期化する文はコンパイルエラーになる。

constexpr関数の引数にconstexpr変数を与えて、戻り値を使用してconstexpr変数を初期化する文はコンパイルできる。
それに対し、constexpr関数の引数に実行時に決まる変数を与えて、戻り値を使用してconstexpr変数を初期化するとコンパイルエラーになる。
これは、コンパイル時に値を決めることができないからである。

 constexpr int twice(const int n)
 {
    return n * 2;
 }
 
 constexpr int ten1 = 10;                                    // コンパイル可能
 constexpr int ten2 = twice(ten1);                           // コンパイル可能
 constexpr int ten3 = twice(static_cast<int>(rand() % 10));  // コンパイルエラー


以下の例では、constexpr関数の引数にconstexprでない変数を与えて、その戻り値でconstexpr変数を初期化しているため、コンパイルエラーになる。
これは、演算結果がROM化できないからである。
それに対し、constexpr関数の引数にconstexprでない変数を与えて、その戻り値でconstexprでない変数を初期化する場合は、コンパイルエラーにならない。

 constexpr int twice(const int n)
 {
    return n * 2;
 }
 
 int ten = 10;
 constexpr int ten2 = twice(ten);  // コンパイルエラー
 const int ten3 = twice(ten);      // コンパイル可能


下表に、ROM化できる条件を示す。なお、初期化する変数はconstexpr変数とする。
下表から分かることは以下の3つである。

  • 渡す引数の指定子は、const / constexprを指定する。
  • 仮引数の指定子には、指定子なし / constを指定する。
  • 関数の指定子は、必ずconstexprを指定する。
引数の指定子 仮引数の指定子 関数の指定子 コンパイルの可否
× - - ×
- - × ×
- - const ×
- constexpr - ×
const × constexpr
const const constexpr
constexpr × constexpr
constexpr const constexpr


上記の例のように、constexpr関数の引数に何を渡すか、constexpr関数の戻り値で何を初期化するかによって、
コンパイル可能またはコンパイルエラーになる。

これは、設計者に対して、ROM化できるソースコードを記述する促すことになる。

constexpr変数への戻り値の代入

constexprは関数にも付加することができる。
関数に付加するconstexprキーワードは、この関数はコンパイル時に計算できることを表している。

constexpr変数に代入できる値は、コンパイル時に計算できる値だけである。
そのため、constexprキーワードの無い関数の戻り値をconstexpr変数にする場合、
コンパイラはこの値はコンパイル時には計算できないと考えて、コンパイルエラーを出力する。
また、constexprキーワードがある関数の戻り値においても、関数の内容を解析して、
同様に、コンパイル時には計算できない式が1つでもあれば、コンパイルエラーを出力する。

 #include <iostream>
 
 // 常に42を返す関数
 auto answer()
 {
    return 42;
 }
 
 // 常に42を返すconstexprな関数
 constexpr auto answer_constexpr()
 {
    return 42;
 }
 
 // 常に42を返すconstexprな関数(?)
 constexpr auto answer_print()
 {
    // std::cout << 42 << std::endl; // コンパイルエラー  標準出力への書き出しはコンパイル時には行えない
    return 42;
 }
 
 int main()
 {
    // constexpr auto a = answer();         // コンパイルエラー  answer関数はconstexpr関数ではない
    constexpr auto b = answer_constexpr();  // コンパイル可能    answer_constexpr関数はconstexpr関数
    // constexpr auto c = answer_stdin();   // コンパイルエラー  answer_stdin関数はコンパイルできない
    std::cout << /* a << ", " << */ b << /* ", " << c << */ std::endl;

    return 0;
 }


また、コンパイル時に計算されているかどうか確認する場合は、static_assertを使用する方法もある。

 // 常に42を返す関数
 auto answer()
 {
    return 42;
 }
 
 // 常に42を返すconstexprな関数
 constexpr auto answer_constexpr()
 {
    return 42;
 }
 
 int main()
 {
    // static_assert(answer() || true);         // コンパイルエラー  answer()はコンパイル時に計算できない
    static_assert(answer_constexpr() || true);  // コンパイル可能    answer_constexpr()はconstexpr関数
 
    return 0;
 }


constexprではない引数を与える

constexpr関数の結果は、常にコンパイル時に計算されるというわけではない。

以下のサンプルコードでは、constexprが付いているanswer_constexpr関数の引数questionはconstexprではないが、
この場合でもコンパイル可能で、answer_constexpr関数はあたかもconstexprキーワードの無い関数のように(少なくとも(1)の行では)動作する。

constexprキーワードはあくまで「コンパイル時に値が確定できる」ことを伝えるだけで、「コンパイル時にしか値を計算しない」というわけではない。

 #include <iostream>
 
 // 標準入力から int 型の値を受け取って返す関数
 auto get_value_from_stdin()
 {
    int v;
    std::cin >> v;
    return v;
 }
 
 // 常に42を返すconstexprな関数
 constexpr auto answer_constexpr([[maybe_unused]] int question)
 {
    return 42;
 }
 
 int main()
 {
    constexpr auto value_constexpr = 1;               // constexprな値
    const auto value_const = get_value_from_stdin();  // constだがconstexprではない値
 
    constexpr auto a = answer_constexpr(value_constexpr);  // コンパイル可能      引数がconstexprなので、コンパイル時に値が確定する
    // constexpr auto b = answer_constexpr(value_const);   // コンパイルエラー    引数がconstexprな値ではないので、コンパイル時に値が確定しない
    const auto c = answer_constexpr(value_constexpr);      // コンパイル可能      引数がconstexprなので、コンパイル時に値が確定する
    const auto d = answer_constexpr(value_const);          // (1) コンパイル可能  引数がconstexprな値ではないので、コンパイル時に値は確定しないが、コンパイルエラーにはならない
 
    std::cout << a << ", " << /* b << ", " << */ c << ", " << d << std::endl;
 
    return 0;
 }


引数や戻り値のconstとconstexpr関数

変数のconstexprがconstを兼ねているので見づらいが、関数のconstexprは引数や返り値のconstとは一切関係が無い。

以下のサンプルコードでは、answer_constexpr3関数は「変数の参照を受け取り、破壊的変更を加えて、その参照をconstも付けずに返す」という操作を行っているが、
この操作は全て(少なくともC++14以降では)constexprで行ってよい操作なので、この関数は問題なくconstexpr関数として作ることができる。

 #include <iostream>
 
 // OK、常に 42 を返す、 constexpr な関数
 constexpr auto answer_constexpr([[maybe_unused]] int question)
 {
    return 42;
 }
 
 // OK、常に 42 を返す、 constexpr な関数
 constexpr auto answer_constexpr2([[maybe_unused]] const int question)
 {
    return 42;
 }
 
 // OK、与えられた引数に 1 を足して返す、 constexpr な関数
 // 受け取る変数を直接書き換えて返すので何も const じゃないように見えるが、れっきとした constexpr 関数である
 constexpr auto& answer_constexpr3(int& question)
 {
    question += 1;
    return question;
 }
 
 // OK、 answer_constexpr3 に直接 constexpr な変数は渡せないが、
 // 「const でない変数を渡して constexpr 関数を用いて計算する」こと自体は constexpr
 constexpr auto use_answer_constexpr3(int question)
 {
    auto q = question;
    q = answer_constexpr3(q);
    return q;
 }
 
 int main()
 {
    constexpr auto value = 41;
    constexpr auto a = answer_constexpr(value);
    constexpr auto b = answer_constexpr2(value);
    constexpr auto c = use_answer_constexpr3(value);
    std::cout << a << ", " << b << ", " << c << std::endl;
 
    return 0;
 }


外部変数の参照

constexpr関数で行なってはいけない操作は、主に引数以外のconstexpr以外の外部の変数を参照する操作である。

 #include <iostream>
 
 // 標準入力からint型の値を受け取って返す(コンパイル時に値が定まらない)関数
 auto get_value_from_stdin()
 {
    int v;
    std::cin >> v;
    return v;
 }
 
 namespace sample
 {
    const auto outer = get_value_from_stdin();  // 外部の変数
    constexpr auto outer_constexpr = 42;        // constexprな外部の変数
 
    // コンパイルエラー  外部の変数を参照しているのでconstexpr関数にできない
    // constexpr auto f_outer()
    // {
    //    return outer;
    // }
 
    // コンパイル可能  constexprな外部の変数は参照してもよい
    constexpr auto f_outer_constexpr()
    {
       return outer_constexpr;
    }
 
    constexpr int& f_argument(int& x)
    {
        x += 1; // OK、引数になら色々やってもよい
        return x;
    }
 
    constexpr auto use_f_argument()
    {
        int x = 41;
        return f_argument(x);
    }
 }
 
 int main()
 {
    // constexpr auto a = sample::f_outer();         // コンパイルエラー  f_outerはコンパイルできない
    constexpr auto b = sample::f_outer_constexpr();
    constexpr auto c = sample::use_f_argument();
 
    std::cout << /* a << ", " << */ b << ", " << c << std::endl;
 
    return 0;
 }


メンバ関数のconstexpr

メンバ関数にもconstexprを付けることができる。
constexprを付ける基準は先ほどと同様、「コンパイル時に計算できるかどうか」である。

 class CHoge
 {
    // ...
    public:
    // ...
    auto f(int x, ...) { ... };
 };

を、以下のように考えれば、constexprを付けるかどうかが判断できる。

 class CHoge {...};
 auto f(C& this, int x, ...) { ... };


 #include <iostream>
 
 class CHoge
 {
    private:
       int a;
 
    public:
       CHoge() = default;
       constexpr CHoge(int a): a(a) {} // メンバ変数の初期化もコンパイル時に可能
 
       // コンパイル可能  コンパイル時に計算可能
       constexpr auto get() const
       {
          return a;
       }
 
       // コンパイル可能  コンパイル時に計算可能
       constexpr auto add(const int b)
       {
          a += b;
          return a;
       }
 
       constexpr auto print() const
       {
          // std::cout << a << std::endl; // コンパイルエラー  コンパイル時に標準出力への出力はできない
          return a;
       }
 };
 
 constexpr auto use_add(int a)
 {
    auto c = CHoge(a);
    c.add(42);
 
    return c;
 }
 
 int main()
 {
    constexpr auto c = CHoge(42);
    std::cout << c.get() << std::endl;
    constexpr auto c2 = use_add(42);
    std::cout << c2.get() << std::endl;
    c2.print();
 
    return 0;
 }


分割ファイルとconstexprとインライン

constexprが付いている関数は、コンパイル時に計算可能でなければならない。
コンパイル時というのはそれぞれの翻訳単位、すなわち、各ファイルをコンパイルしている場合でも関数が計算できなければならないということである。

したがって、constexprが付いている関数の宣言だけをして、別ファイルで実装を行うことはできない。
実装されているファイル以外をコンパイルしている時に、関数の内容が計算できないからである。

つまり、constexprが付いている関数は自動的にinline関数として扱われるということになる。
また、constexprが付いている変数もinline変数として扱われる。

 // main.cpp
 #include <iostream>
 #include "main.h"
 
 int main()
 {
    // コンパイル可能  int CHoge1::f(int&)は通常のメンバ関数
    auto c1 = CHoge1(41);
    auto x1 = 1;
    std::cout << c1.f(x1) << std::endl;
 
    // コンパイルエラー  int CHoge2::f(int&)はconstexprであるが実装が無い
    // auto c2 = CHoge2(41);
    // auto x2 = 1;
    // std::cout << c2.f(x2) << std::endl;
 
    return 0;
 }


 // main.h
 class CHoge1
 {
    private:
       int a;
 
    public:
       constexpr CHoge1(int a) : a(a) {}
       int f(int& x);
 };
 
 class CHoge2
 {
    private:
       int a;
 
    public:
       constexpr CHoge2(int a) : a(a) {}
       constexpr int f(int& x);
 };


 // sub.cpp
 #include "main.h"
 
 int CHoge1::f(int& x) 
 {
    x += a;
 
    return x;
 }
 
 constexpr int CHoge2::f(int& x) 
 {
    x += a;
 
    return x;
 }


constexprテンプレート関数

constexprが付いているテンプレート関数の場合、その関数を実体化した時にconstexpr関数として不適格な場合でもコンパイルエラーにはならず、非constexpr関数として扱われる。
以下のサンプルコードの場合、print_and_get関数はどのように実体化してもconstexpr関数にはならないが、コンパイルエラーにもならずに、constexprキーワードが無視される。

つまり、テンプレート関数にconstexprキーワードが付いていても、設計者は「関数はconstexpr関数ではないかもしれない」と身構えなければならない。

おそらく、「テンプレートのいくつかの場合はconstexpr、他の場合はconstexprではないような関数を作成したい」という需要があるからだと推測する。

 #include <iostream>
 
 // コンパイルエラー  constexprではない関数
 // constexpr auto print_and_get_int(int t)
 // {
 //    std::cout << t << std::endl;
 //    int v = 0;
 //    std::cin >> v;
 // 
 //      return v;    
 // }
 
 // コンパイル可能  constexprではないテンプレート関数
 // constexprを付けているがコンパイルできる
 template<typename T>
 constexpr auto print_and_get(T t)
 {
    std::cout << t << std::endl;
    int v = 0;
    std::cin >> v;
 
    return v;
 }
 
 int main()
 {
    const auto a = print_and_get(42);         // コンパイル可能    constexprキーワードは無視される
    // constexpr auto b = print_and_get(42);  // コンパイルエラー  constexprで受けるとコンパイルエラー
 
    std::cout << a << /* ", " << b << */ std::endl;
 
    return 0;
 }


constexprが付いているラムダ式

ラムダ式(のoperator())も、関数と同様にconstexprを指定することができる。(指定しなくとも自動的にconstexprが付けてくれる)

 #include <iostream>
 
 // 標準入力からint型の値を受け取って返す(コンパイル時に値が定まらない)関数
 auto get_value_from_stdin()
 {
    int v;
    std::cin >> v;
 
    return v;
 }
 
 int main()
 {
    // 常に42を返すラムダ式
    auto f = []{ return 42; };
 
    // 常に42を返すconstexprを明示したラムダ式
    auto f_constexpr = []() constexpr { return 42; };
 
    // constexprが付いている変数はキャプチャ可能
    constexpr auto outer_value = 42;
    auto f_capture = [&outer_value]{ return outer_value; };
 
    // constexprが付いている変数をキャプチャした場合は、constexpr関数にはならない
    const auto outer_value2 = get_value_from_stdin();
    auto f_bad = [&outer_value2]{ return outer_value2; };
 
    constexpr auto a = f();            // constexpr関数であることを明示しなくても使用可能
    constexpr auto b = f_constexpr();  // constexpr関数であることを明示しても使用可能
    constexpr auto c = f_capture();    // コンパイル可能    この関数はconstexpr関数
    // constexpr auto d = f_bad();     // コンパイルエラー  この関数はconstexpr関数ではない
 
    std::cout << a << ", " <<  b <<  ", " << c << /* ", " << d << */ std::endl;
 
    return 0;
 }



レビュー

  1. constが付加された変数を見つけた場合、その変数はRAMにしか配置できないものか確認する。
    もし、ROM化できる場合、constexpr指定子に変更できないか考えて指摘する。
  2. constexprテンプレート関数の内部に、ROM化できない文や関数呼び出しが入っていないか確認する。
  3. ESCRでは再帰は使用してはいけないので、constexpr関数内において、再帰を使用してはいけない。
  4. constexpr関数やconstexpr関数テンプレートを見つけた場合、その戻り値で初期化している変数とconstexpr型指定されているかどうか確認する。
    そうでなければ、constexpr関数やconstexpr関数テンプレートの存在意義がない。


以下の例は、constexprを付加しても意味の無い例である。
ただし、コンパイルエラーにはならない。

 template <typename T>
 constexpr inline static T GetFromCin()
 {
    T value;
    cin >> value;
    return value;
 }



まとめ

  • constexprは、変数と関数で挙動が大きく異なるので、同一視しない。
  • constexpr関数とconst修飾子にあまり関係がない点にも注意する。
  • 複雑な関数の場合、「外部に影響を与えるか」「inlineにできるか」の2点を考えれば、その関数がconstexprが必要かどうか分かる。
  • constexprを付けることができる関数の幅は広いので使用するべきである。
    ただし、テンプレート関数をconstexprにするときは注意が必要である。