最終更新日時(UTC):
が更新

履歴 編集

constexpr if 文(C++17)

概要

constexpr if文とは、コンパイル時に条件分岐する文である。

if constepxr ( condition )
  statement
else
  statement

constexpr if文の導入によってC++のif系の条件分岐文は3種類になった。

  • プリプロセス時if: #if
  • コンパイル時if: constexpr if
  • 実行時if: if

conditionはコンパイル時にコンテキスト依存のbool型への変換が起こる式である。例えばconstexpr指定されたexplicit operator bool()は評価できる。

else文にはconstexpr書かない。ifの後にconstexprを書く以外では実行時if文と構文に差はない。

プリプロセス時if文と異なり、constexpr if文は条件付きコンパイルをすることはできない。例えば次の例は不適格である。

struct X {
  if constexpr (cond) {
    void f();
    using int32 = int;
  }
  else {
    void g();
  }
};

constexpr if文はスコープを作るので、例えばVisual C++の独自拡張機能である__if_existsは以下のような書き方が可能であるが、constexpr if文でこれを再現することはできない。

struct A {
  static float get() { return 1.2f; }
};
int main() {
  auto a = __if_exists(A::get) {
    A::get();
  }
  __if_not_exists(A::get) {
    "not found";
  }
}

同様にD言語のstatic ifとは違いスコープを作るので、D言語で可能な次のようなことはconstexpr if文で再現できない。

const int i = 3;
class C {
  const int k = 5;

  static if (i == 3) // D言語ではok
    int x;
  else
    long x;
}

constexpr if文で、実行されない方のstatementdiscarded statement(破棄文)となり、文の実体化を防ぐ。言い換えると、Two Phase Name Look-upにおけるdependent name(以下、依存名)は、discarded statementの場合検証されない。また文が実体化されないのだから通常のif文と同じくもちろん実行時に実行もされない。つまり次の例は意図と異なる挙動を示す。

#include <type_traits>

template <typename T>
void f()
{
  if constexpr (std::is_same_v<T, int>)
  {
    // Tがintのときのみ評価されてほしい
    // 実際は常に評価される
    static_assert(false);
  }
}

int main()
{
  f(2.4);
  f(3);
}

なぜならばdiscarded statementはテンプレートの実体化を防ぐ (依存名の検証をしない) だけで、非依存名は検証されるからである。この例のstatic_assertに渡す条件式はテンプレートパラメータに依存していないので、テンプレートの宣言時に検証され、エラーとなる。言い換えればstatic_assertに渡す条件式が依存名ならばテンプレートの宣言時に検証されず、テンプレート実体化まで評価を遅らせることができる。

#include <type_traits>

template <typename T>
constexpr bool false_v = false;

template <typename T>
void f(T)
{
  if constexpr (std::is_same_v<T, int>)
  {
    // Tがintのときのみ評価される
    static_assert(false_v<T>);
  }
}

int main()
{
  f(2.4);
  f(3);
}

上の例ではfalse_vを作ったが、ラムダ式でも同様のことができる。ラムダ式はそれが記述された位置から見て最小のスコープ (ブロックスコープ/クラススコープ/名前空間スコープ) で宣言されるクラスとして扱われる。例えば、下の例ではf()という関数テンプレート内で宣言される。関数テンプレート内のクラスは依存名になるため、テンプレートの宣言時に検証されず、テンプレート実体化まで評価を遅らせることができる。

#include <type_traits>

template <typename T>
void f(T)
{
  if constexpr (std::is_same_v<T, int>)
  {
    // Tがintのときのみ評価される
    static_assert([]{return false;}());
  }
}

int main()
{
  f(2.4);
  f(3);
}

constexpr if文の条件式内は実体化が起きる。したがって実体化するとコンパイルエラーになるものは書いてはいけない。

#include <type_traits>
#include <iostream>

struct Hoge {
  using type = int;
};

template <typename T>
void f()
{
  if constexpr (std::is_same_v<T::type, int> || std::is_same_v<T::value_type, int>) {
    std::cout << "is int" << std::endl;
  }
}

int main()
{
  f<Hoge>(); //error: Hoge::value_typeは存在しないのでif constexpr文の条件式がコンパイルエラーになる
}

なお型情報のifが欲しいならば、std::conditional がある。

#include <type_traits>
#include <random>
#include <cstdint>
#include <iostream>

// C++11
template<typename Integer>
using mt = typename std::conditional<std::is_same<Integer, std::uint32_t>::value, std::mt19937, std::mt19937_64>::type;

// C++14以降
// template<typename Integer>
// using mt = std::conditional_t<std::is_same<Integer, std::uint32_t>::value, std::mt19937, std::mt19937_64>;

int main()
{
  mt<std::uint32_t> m1 {37};
  std::cout << m1() << std::endl;
}

関連項目

参照