LoginSignup
101
89

More than 5 years have passed since last update.

C++、constexprのまとめ

Posted at

はじめに

C++にはconstexprという概念がある。
これまでよくわかっていなかったのだが、きちんと調べてconstexprを理解したつもりになったので、ここにまとめる。

(以下の話は、全てC++17以降を想定している。)

話の要点

constexprを使えない・使うべきでない主な場面

変数

  • constでない変数
  • クラスのメンバ変数
  • 標準入力などの非constexpr関数を用いて計算する値
  • 引数などのconstexprでない可能性がある値を用いて計算する値

関数

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

constexprを使うべき主な場面

  • 上記以外全て

変数のconstexpr

変数におけるconstexprは、#defineなどで作っていたようなコンパイル時定数を作るためのキーワードである。

#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; // OK、 a は後から書き換えてよい
    // b = 5; // NG、 b は const なので書き換えてはいけない
    // c = 6; // NG、 c は const なので書き換えてはいけない
    std::cout << a << ", " << b << ", " << c << std::endl;

    auto d = get_value_from_stdin(); // OK、 d は実行時に受け取った値
    const auto e = get_value_from_stdin(); // OK、 e は実行時に受け取り、今後変更されない値
    // constexpr auto f = get_value_from_stdin(); // NG、 f はコンパイル時に値が確定しなければならない
    std::cout << d << ", " << e << /* ", " << f << */ std::endl;

    return 0;
}

constが「この変数は今後変更しませんし、変更しようとしたらコンパイルエラーにしてくださいね」という合図であるのに対し、変数に付けるconstexprは「この変数の値はコンパイル時に確定するし、確定しなければコンパイルエラーにしてくださいね」という合図である。
constexpr変数はconst変数としても扱われる。

コンパイラは、constexprがついた変数の値をコンパイル時に計算しようとする。もし計算できなければ、コンパイルエラーを吐く。
上のコードでは、「標準入力から受け取る」という操作がコンパイル時に行えないため、エラーとなっている。

では、どの操作が「コンパイル時に計算できる」のだろうか?
後述する通り、それは関数にconstexprキーワードがついているかどうかで判断されるのである。

関数のconstexpr

constexpr変数への返り値の代入

constexprは関数にも付けることができる。
関数につけたconstexprキーワードは、「この関数はコンパイル時に計算することもできますよ」ということを表している。

#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; // NG、 標準出力への書き出しはコンパイル時には行えない
    return 42;
}

int main(){
    // constexpr auto a = answer(); // NG、 answer 関数は constexpr 関数ではない
    constexpr auto b = answer_constexpr(); // OK、 answer_constexpr 関数は constexpr 関数
    // constexpr auto c = answer_stdin(); // NG、 answer_stdin 関数はコンパイルできない
    std::cout << /* a << ", " << */ b << /* ", " << c << */ std::endl;

    return 0;
}

constexpr変数に入れられる値は、コンパイル時に計算できる値だけである。
だから、constexprキーワードのない関数の返り値をconstexpr変数に入れようとすると、コンパイラは「この値はコンパイル時には計算できない」と考え、constexpr変数には入れられないよと怒ってくれる。
また、constexprキーワードがある関数の返り値については、関数の中身を見て値を計算しようとし、その過程で同様に「コンパイル時には計算できない」式がひとつでもあれば、やはりconstexpr変数には入れられないよと怒ってくれる。

また、コンパイル時に計算されているかどうか確認したければ、static_assertを用いるという手もある。

// 常に 42 を返す関数
auto answer() { return 42; }

// 常に 42 を返す、 constexpr な関数
constexpr auto answer_constexpr() { return 42; }

int main(){
    // static_assert(answer() || true); // NG、 answer() はコンパイル時に計算できない
    static_assert(answer_constexpr() || true);

    return 0;
}

constexprではない引数を与える

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); // OK、引数が constexpr なので、コンパイル時に値が確定する
    // constexpr auto b = answer_constexpr(value_const); // NG、引数が constexpr な値ではないので、コンパイル時に値が確定せず、コンパイルエラー
    const auto c = answer_constexpr(value_constexpr); // OK、引数が constexpr なので、コンパイル時に値が確定する
    const auto d = answer_constexpr(value_const); // (1) OK、引数が constexpr な値ではないので、コンパイル時に値は確定しないが、コンパイルエラーにはならない

    std::cout << a << ", " << /* b << ", " << */ c << ", " << d << std::endl;

    return 0;
}

この例では、constexpr関数の引数value2constexprではない。
この場合でもコンパイルは成功し、answer_constexprはあたかもconstexprキーワードのない関数かのように(少なくとも(1)の行では)振る舞う。

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

引数や返り値のconstconstexpr関数

変数のconstexprconstを兼ねているのでわかりにくいのだが、関数のconstexprは引数や返り値のconstとは一切関係がない。

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

上の例では、answer_constexpr3関数は「変数の参照を受け取り、破壊的変更を加えて、その参照をconstも付けずに返す」という、およそconst性の欠片もない操作を行っている。
しかしながら、この操作は全て(少なくともC++14以降では)constexprで行ってよい操作なので、この関数は問題なく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 auto f_outer() { return outer; } // NG、外部の変数を参照しているので constexpr 関数にできない

    constexpr auto f_outer_constexpr() { return outer_constexpr; } // OK、 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(); // NG、 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を付けることができる。
付ける基準は先ほどと同様、「コンパイル時に計算できるかどうか」である。

クラスCのメンバ関数

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

を、

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

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

#include <iostream>

class C
{
public:
    C() = default;
    constexpr C(int a): a(a) {} // メンバ変数の初期化もコンパイル時に可能

    // OK、コンパイル時に計算可能
    constexpr auto get() const
    {
        return a;
    }

    // OK、コンパイル時に計算可能
    constexpr auto add(const int b)
    {
        a += b;
        return a;
    }

    // NG、コンパイル時に標準出力への出力はできない
    constexpr auto print() const
    {
        // std::cout << a << std::endl; // NG
        return a;
    }

private:
    int a;
};

constexpr auto use_add(int a)
{
    auto c = C(a);
    c.add(42);
    return c;
}

int main(){
    constexpr auto c = C(42);
    std::cout << c.get() << std::endl;
    constexpr auto c2 = use_add(42);
    std::cout << c2.get() << std::endl;
    c2.print();

    return 0;
}

分割ファイルとconstexprinline

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

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

main.cpp
#include <iostream>
#include "c.hpp"

int main(){
    // OK、 int C1::f(int&) は普通のメンバ関数
    auto c1 = C1(41);
    auto x1 = 1;
    std::cout << c1.f(x1) << std::endl;

    /* NG、 int C2::f(int&) は constexpr なのに実装がない
    auto c2 = C2(41);
    auto x2 = 1;
    std::cout << c2.f(x2) << std::endl;
    */

    return 0;
}
c.hpp
class C1
{
public:
    constexpr C1(int a): a(a) {}
    int f(int& x);
private:
    int a;
};

class C2
{
public:
    constexpr C2(int a): a(a) {}
    constexpr int f(int& x);
private:
    int a;
};
c.cpp
#include "c.hpp"

int C1::f(int& x) 
{
    x += a;
    return x;
}
constexpr int C2::f(int& x) 
{
    x += a;
    return x;
}

というか、constexpr関数は自動的にinline関数として扱われるので、こういうことになるのである。
あまり意識しなくていいとは思うが、constexpr変数もinline変数として扱われる。

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); // 残念ながら OK、constexprキーワードは無視される
    // constexpr auto b = print_and_get(42); // NG、constexpr で受けるとコンパイルエラー
    std::cout << a << /* ", " << b << */ std::endl;

    return 0;
}

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

なので、テンプレート関数にconstexprキーワードがついていても、プログラマは「もしかしたらこの関数は(常に)constexpr関数ではないかもしれない」と身構えなければならない。
おそらく、テンプレートのいくつかの場合でconstexprで、かつ他の場合でconstexprではないような関数を作りたい、という需要があるからだと思うのだが、そこは特殊化で対応してほしかったという気持ちである。

constexprなラムダ式

ラムダ式(のoperator())も、関数と同様にconstexprに指定することができる。
というか、指定しなくても勝手にconstexprになってくれる。

#include <iostream>

// 標準入力から int 型の値を受け取って返す、(コンパイル時に値が定まらない)関数
auto get_value_from_stdin()
{
    int v;
    std::cin >> v;
    return v;
}

int main(){
    // 常に 42 を返す lambda 式
    auto lambda = []{ return 42; };
    // 常に 42 を返す、 constexpr を明示した lambda 式
    auto lambda_constexpr = []() constexpr { return 42; };
    // constexpr な変数はキャプチャ可能
    constexpr auto outer_value = 42;
    auto lambda_capture = [&outer_value]{ return outer_value; };
    // constexpr でない変数をキャプチャした場合、 constexpr 関数にはならない
    const auto outer_value2 = get_value_from_stdin();
    auto lambda_bad = [&outer_value2]{ return outer_value2; };

    constexpr auto a = lambda(); // constexpr 関数であることを明示しなくても使える
    constexpr auto b = lambda_constexpr(); // 当然明示してもよい
    constexpr auto c = lambda_capture(); // OK、この関数は constexpr 関数
    // constexpr auto d = lambda_bad(); // NG、この関数は constexpr 関数ではない
    std::cout << a << ", " <<  b <<  ", " << c << /* ", " << d << */ std::endl;

    return 0;
}

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
101
89