C++
C++17
C++Day 20

みんなも使おうif constexpr

この記事は C++ Advent Calendar 2018 の20日目の記事です.
昨日は @Kogia_sima さんの「 filesystemの標準入りが嬉しすぎてライブラリを作った話 」でした.filesystemの標準入りが嬉しい,わかる.

修論に手がついてなくて大分やばいため,今年の内容は簡素なものになりました1

平成最後のAdCですが2,C++17の if constexpr が便利という話をします3
正直「わぁ便利だね!」って言うような人はもう知ってる内容な気がするんですけど…

if constexpr is 何

方々で「 if constexpr はコンパイル時if文ではない!!!!!!!!!!!!!!!!!」とか言われたりして,「じゃあなんなんだよ」って思ってたんですが,ざっくり言うと

特殊化
template<bool Cond>
void f(){
  // when Cond is true
}

template<>
void f<false>(){
  // when Cond is false
}

を,

if_constexpr
template<bool Cond>
void f(){
  if constexpr(Cond){
    // when Cond is true
  }
  else{
    // when Cond is false
  }
}

と書ける機能です.以上.

「コンパイル時if文ではない」とは

結局,

if_constexpr
template<bool Cond>
void f(){
  if constexpr(Cond){
    // Condがtrueなら落としたい
    static_assert(false, "Cond must be false");
  }
  else{
    // when Cond is false
  }
}

とか書くと,

特殊化
template<bool Cond>
void f(){
  // Condがtrueなら落としたい
  static_assert(false, "Cond must be false");
}

template<>
void f<false>(){
  // when Cond is false
}

と書いたのと一緒なので,当然Two Phase Lookupの1巡目でstatic assertion failedで終了します.
結局,「条件付きコンパイル」(条件を満たさないコードは コンパイルしない )ではなく「templateのインスタンス化の制御」(条件を満たさないコードは templateのインスタンス化の際に無視される (インスタンス化の前に落ちるコードは落ちる))でしかない,というあたりの話です.
この辺の詳細は 江添さんのブログ が詳しい.

言うほど便利?

まぁ小さいコードだと伝わりにくいんですけど,結構色々便利です.

関数定義が複雑なとき

例えば関数の引数やtemplate引数がメチャクチャ長かったり特殊化がメチャクチャ多かったりした場合を考えてみましょう.

特殊化
template<typename... Ts>
struct types_holder{};

template<typename A, typename B, typename C, typename D, typename E, typename F, /*bool Cond1,*/ bool Cond2, bool Cond3>
bool f(A&&, B&&, C&&, D&&, E&&, F&&, types_holder<std::bool_constant</*Cond1 ==*/ true>, std::bool_constant<Cond2>, std::bool_constant<Cond3>>){
  // when Cond1 is true
}

template<typename A, typename B, typename C, typename D, typename E, typename F, /*bool Cond1,*/ bool Cond2, bool Cond3>
bool f(A&&, B&&, C&&, D&&, E&&, F&&, types_holder<std::bool_constant</*Cond1 ==*/ false>, std::bool_constant<Cond2>, std::bool_constant<Cond3>>){
  // when Cond1 is false
}

特殊化のたびに増えるのはちょっと嫌ですね.さらに,ここに「 Cond2 && std::is_pointer_type_v<std::decay_t<B>> か否かでSFINAEをしましょう」とか盛り盛りされていくと収拾がつかなさそうです.

if_constexpr
template<typename A, typename B, typename C, typename D, typename E, typename F, bool Cond1, bool Cond2, bool Cond3>
bool f(A&&, B&&, C&&, D&&, E&&, F&&, types_holder<std::bool_constant<Cond1>, std::bool_constant<Cond2>, std::bool_constant<Cond3>>){
  static constexpr bool ComplicatedCond = Cond2 && std::is_pointer_type_v<std::decay_t<B>>;
  if constexpr(ComplicatedCond){
    // when ComplicatedCond is true
  }
  else if constexpr(Cond1){
    // when Cond1 is true and Complicated Cond is false
  }
  else{
    // when Cond1 is false and Complicated Cond is false
  }
}

いくらかマシになった気はします4

部分的にコードを変えたいとき

特殊化
template<bool Cond1, bool Cond2>
void f(){
  // なんか処理
  do_something1();
  // なんか処理
}

template<>
void f<true, false>(){
  // なんか処理
  do_something2();
  // なんか処理
}

template<>
void f<false, true>(){
  // なんか処理
  do_something3();
  // なんか処理
}

template<>
void f<false, false>(){
  // なんか処理
  do_something4();
  // なんか処理
}
if_constexpr
template<bool Cond1, bool Cond2>
void f(){
  // なんか処理
  if constexpr(Cond1)
    if constexpr(Cond2)
      do_something1();
    else
      do_something2();
  else
    if constexpr(Cond2)
      do_something3();
    else
      do_something4();
  // なんか処理
}

当然ですが特殊化でやると(//なんか処理 部分の)コードクローンが条件の数 $N$ に対して最大 $2^N$ に増えるので,template引数によって変更されるコードが細かい場合は if constexpr のほうが良いでしょう.

特殊化だと解決が曖昧になるとき

特殊化
template<typename T, typename U>void f(T, U){/*T, U*/}
template<typename T>
void f(T, T){/*T, T*/}
template<typename... Ts, typename U>
void f(std::tuple<Ts...>, U){/*Ts..., U*/}
template<typename T, typename... Us>
void f(T, std::tuple<Us...>){/*T, Us...*/}
template<typename... Ts, typename... Us>
void f(std::tuple<Ts...>, std::tuple<Us...>){/*Ts..., Us...*/}

この ff(std::tuple<Ts...>, std::tuple<Ts...>) の解決に失敗します(すべての特殊化が候補として同率に並ぶため).
簡単に解決する方法としては

特殊化
template<typename... Ts>
void f(std::tuple<Ts...>, std::tuple<Ts...>){/*Ts..., Ts...*/}

を追加すれば良いのですが, f(T, T)f(std::tuple<Ts...>, std::tuple<Us...>) が選択されれば書かなくてよかったコードクローンが増えてしまいます.
また,

template<std::size_t N>struct priority : priority<N-1>{};
template<>struct priority<0>{};

を使って解決順位を明示する方法もありますが,メチャクチャ候補が多いとtemplateの再帰深度がこわいですね5

if_constexpr
template<typename> struct is_tuple : std::false_type{};
template<typename... Ts> struct is_tuple<std::tuple<Ts...>> : std::true_type{};

template<typename T, typename U>
void f(T, U){
  if constexpr(std::is_same_v<T, U>){
    /*T, T*/
  }
  else if constexpr(std::is_tuple<T>::value){
    if constexpr(std::is_tuple<U>::value){
      /*Ts..., Us...*/
    }
    else{
      /*Ts..., U*/
    }
  }
  else if constexpr(std::is_tuple<U>::value){
    /*T, Us...*/
  }
  else{
    /*T, U*/
  }
}

そもそも if constexpr であれば端から 上から順番に試す のでそんな心配とはおさらばです.
まぁ,templateのパターンマッチが使えないためメタ関数が必要になったりするので元が enable_if だらけのSFINAE分岐でなかった場合は手放しでは喜べないかもしれませんが,複数の条件が複雑に絡んで条件を重複なく記述するのが面倒な場合に手続き的に書けると楽なときもあります.

そもそも特殊化できないとき

関数の特殊化ってクラススコープ内だとできないんですよ.

特殊化
struct T{
  template<bool>void f();
  /* ここで
  template<>void f<true>(){}
     とか書くと怒られが発生
  */
};

//クラススコープでなければメンバ関数の特殊化自体は可能
template<>
void T::f<true>(){do_something1();}

template<>
void T::f<false>(){do_something2();}

クラス定義はクラス内にまとめたい衝動に駆られるので,これは困ります.

if_constexpr
struct T{
  template<bool Cond>
  void f(){
    if constexpr(Cond)
      do_something1();
    else
      do_something2();
  }
};

うれしい.

他に,関数templateの部分特殊化も許されていません.

特殊化
template<typename T, bool Cond>
void f(){}

template<typename T>
void f<T, false>(){} // ダメ

if constexpr ならいけます.

if_constexpr
template<typename T, bool Cond>
void f(){
  if constexpr(Cond){
    // when Cond is true
  }
  else{
    // when Cond is false
  }
}

便利ですね.

こんなときはどうしたらいいの?

1つしか浮かばなかった.そのうち追記するかもしれない.

戻り値型を変えたいよ

現代では戻り値型はコンパイラが推論できる.便利な時代だ.

if_constexpr
template<bool Cond>
auto f(){
  if constexpr(Cond)
    return 0;
  else
    return 3.14f;
}

これを使うと 構造化束縛用の get メンバ関数の定義がメチャクチャ楽になります

構造化束縛用のget
class T{
  int a;
  float b;
  std::string c;
 public:
  template<std::size_t N>
  decltype(auto) get()const{
    static_assert(N <= 2);
         if constexpr(N == 0) return a;
    else if constexpr(N == 1) return b;
    else if constexpr(N == 2) return (c);
  }
};

まとめ

みんなも使おう if constexpr


明日は@gnaggnoyilさんです.


  1. 去年も学会準備してたらAdCの期日過ぎて,数日後に書こうと思ったら代理投稿が入ってしまってたので… 

  2. #新元号をさっさと公表しろ日本政府 

  3. 「例年ライブラリ作ってたのに今年は無いんか」と思われた方,今年もライブラリは作った(際に「 if constexpr 便利やんけ」となった)んですけど,なんかこう…喜ぶ人が少なそうなライブラリなのと,ライブラリ紹介と if constexpr の2本立てにすると記事のまとまりに欠けるということで,ライブラリ紹介は消しました. 

  4. まぁこれをやるとクソ長関数が爆誕してしまうので,なんか上手いことやる必要はある.引数が増えまくるのは避けられないけどわかりやすい関数名にして if constexpr を書く関数はディスパッチに徹するとか…?(SFINAEよりは見た目が幾分マシ,後述するクラススコープ内でも利用可能などメリットはある) 

  5. 再帰深度を気にするほど候補を増やすことはそうないと思いますが…