[C++]explicit(bool)の使いどころ

※この内容はC++20から利用可能になる予定の情報であり、一部の内容が変更される可能性があります。

explicit(bool)指定子

従来コンストラクタと変換関数に付けることのできていたexplicit指定子は 、C++20よりexplicit(expr)という形のものに置き換えられます。
かっこの中のexprに指定出来るのはboolに変換可能な定数式です。

そして、そのexprtrueとなる場合はそのコンストラクタ(変換関数)はexplictコンストラクタになり、falseとなる場合は通常の非explicitコンストラクタとして扱われます。

また、従来の式を指定しないexplicit指定はexplicit(true)として扱われるようになります。

データメンバ型のexplicit性継承

ではこのexplicit(bool)指定子、一体何が嬉しいのでしょうか?

それを知るために、以下のようにテンプレートによって任意の型を保持するようなクラスを考えてみます。

template<typename T>
struct unit {

  //Tに変換可能なUから構築
  template<typename U=T>
  constexpr unit(U&& other)
    : value(std::forward<U>(other))
  {}

  T value;
};

とりあえず最小限のコンストラクタをそろえておきます。
最小限であるとはいえこれで目的を達することができ、特に問題は無いように思えます。

そこで、このunit型にexplicitコンストラクタを持つ型を入れてみましょう。

struct has_explicit_ctor {
  explicit has_explicit_ctor(int) {}
};

int main() {
  has_explicit_ctor s1{1};     //ok、直接初期化(明示的コンストラクタ呼び出し)
  has_explicit_ctor s2 = {2};  //ng、コピーリスト初期化(暗黙変換)
  has_explicit_ctor s3 = 3;    //ng、コピー初期化(暗黙変換)

  unit<has_explicit_ctor> s4 = {10};  //ok
  unit<has_explicit_ctor> s5 = 10;    //ok
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ご覧のように、包むunit型のコンストラクタを通すことによって、要素型has_explicit_ctorのexplicitコンストラクタのexplicit性が失われてしまっています。
その結果、初期化時に暗黙変換が行われるようになってしまっています・・・

暗黙変換されたくないのでコンストラクタにexplicitを付けているはずで、この様に別の型に包まれたとしても同じようになってくれなければ困ってしまいます。

では、unitのコンストラクタにexplicitを付加してやりましょう。そうすれば解決ですね。

template<typename T>
struct unit {

  //Tに変換可能なUから構築
  template<typename U=T>
  explicit constexpr unit(U&& other)
    : value(std::forward<U>(other))
  {}

  T value;
};

int main() {
  unit<has_explicit_ctor> u1 = {10};  //ng
  unit<has_explicit_ctor> u2 = 10;    //ng
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

しかしこれにも問題があります。

auto f() -> unit<int> {
  return {128};  //compile error!
}

int main() {
  unit<int> u1 = {10};  //ng
  unit<int> u2 = 10;    //ng
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

はい、今度は非explicitコンストラクタを持つ型を入れた時に意図しないコンパイルエラーが多発します。すべてunit型のコンストラクタがexplicitであるためです。
(これがまさに、C++17においてN4387で解決されたstd::tuplestd::pairのコンストラクタの問題です。)

これを解決する一つの策が、型引数Tのコンストラクタのexplicit性を継承するPerfect Initializationと呼ばれるイディオムです。

template<typename Cond>
using enabler = std::enable_if_t<Cond::value, std::nullptr_t>;

template<typename Cond>
using disabler = std::enable_if_t<!Cond::value, std::nullptr_t>;

template<typename T>
struct unit {

  //Tに暗黙変換可能なUから構築
  template<typename U=T, enabler<std::is_convertible<U, T>> = nullptr>
  constexpr unit(U&& other)
    : value(std::forward<U>(other))
  {}
  
  //Tにexplicitに変換可能なUから構築
  template<typename U=T, disabler<std::is_convertible<U, T>> = nullptr>
  explicit constexpr unit(U&& other)
    : value(std::forward<U>(other))
  {}

  T value;
};

auto f() -> unit<int> {
  return {128};  //ok
}

int main() {
  unit<has_explicit_ctor> s1 = {10};  //ng
  unit<has_explicit_ctor> s2 = 10;    //ng
  
  unit<int> u1 = {10};  //ok
  unit<int> u2 = 10;    //ok
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

std::is_convertible<U, T>UからTに暗黙変換可能かどうかを調べるものです。
それを用いて、暗黙変換可能かどうかでSFINAEして、同じコンストラクタを型Tに応じてexplicitかどうかを切り替えます。割と泥臭い・・・
このようにすることで、このunit<T>のような型は内包する型のexplicit性を継承することができます。

しかし、要するにexplicitの有無の違いだけでなぜ二つも同じコンストラクタを書かなければならないのでしょうか。また、さらにunit<U>const U&などから変換するようなコンストラクタを追加すると同じことをしなければなりません。
これは面倒です、どうにかしたい・・・

そこでようやくexplicit(bool)の出番です。これを使うと、unit型のコンストラクタは簡単になります。

template<typename T>
struct unit {

  //Tに変換可能なUから構築
  template<typename U=T>
  explicit(std::is_convertible_v<U, T> == false)
  constexpr unit(U&& other)
    : value(std::forward<U>(other))
  {}
  
  T value;
};

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

少し見辛い所はありますがexplicit(bool)に暗黙変換可能かどうかを判定する式を渡してやることで、一つのコンストラクタの定義だけで先ほどと同じ効果を得られます。
もはや訳の分からないSFINAEによる分岐は必要ありません。

explicit(bool)はおそらくこの問題のためだけに導入されました。ライブラリで複雑な型を書く場合にはありがたみを感じることができるでしょう・・・

ところで、STLにはこのように内部に任意の型のオブジェクトを保持するようなクラスがいくつかあります。std::tuplestd::pairstd::optionalがその代表です。
std::pairstd::optionalのある1つのコンストラクタを見てみます(std::tupleは説明するには複雑になるのでスキップ・・・)。

std::pairの場合

同じ問題が起きるstd::pairのコンストラクタの(5)を見てみます。

このコンストラクタには要件があります。

(5) : is_constructible<first_type, U&&>::value && is_constructible<second_type, V&&>::valueであること

これを考慮したうえでPerfect Initializationすると、おおよそ以下の様な宣言になります。

//与えられた条件全てがtrueの場合に有効化
template<typename... Cond>
using enabler = std::enable_if_t<std::conjunction<Cond...>::value, std::nullptr_t>;

template<typename T1, typename T2>
struct pair {

  template<class U=T1, class V=T2,
    enabler<
        std::is_constructible<T1, U&&>,
        std::is_constructible<T2, V&&>,
        std::is_convertible<U, T1>,
        std::is_convertible<V, T2>
    > = nullptr
  >
  constexpr pair(U&& x, V&& y);
  
  template<class U=T1, class V=T2,
    enabler<
        std::is_constructible<T1, U&&>,
        std::is_constructible<T2, V&&>,
        std::negation<
          std::conjunction<
            std::is_convertible<U, T1>,
            std::is_convertible<V, T2>
          >
        >
    > = nullptr
  >
  explicit constexpr pair(U&& x, V&& y);

};

先ほどのunitとやってることは同じです。型が二つに増えたので複雑になってしまいました。
要はT1,T2どちらかの型が暗黙変換不可であるとき、コンストラクタにexplicitを付けます。

explicit(bool)を使うと・・・

template<typename T1, typename T2>
struct pair {

  template<class U=T1, class V=T2,
    enabler<
        std::is_constructible<T1, U&&>,
        std::is_constructible<T2, V&&>
    > = nullptr
  >
  explicit(!(std::is_convertible_v<U, T1> && std::is_convertible_v<V, T2>))
  constexpr pair(U&& x, V&& y);

};

要件を判定しSFINAEする部分と、Perfect Initializationする部分とが分離していくらか見やすくなりました。そして1つのコンストラクタ定義で済むようになります。

std::optionalの場合

同じ問題が起きるstd::optionalのコンストラクタの(7)を見てみます。

このコンストラクタには要件があります。

型Tの選択されたコンストラクタがconstexprであれば、このコンストラクタもconstexprとなる
型Uから型Tがムーブ構築可能でなければ、このオーバーロードオーバーロード解決の候補から除外される
型Uから型Tに暗黙的に型変換ができる場合、このオーバーロードは非explicitとなる。
型Uから型Tに明示的な型変換ならできる場合、このオーバーロードはexplicitとなる

§23.6.3.1 Constructors [optional.ctor] - N4659も見るとより詳細が分かります。

これを考慮したうえでPerfect Initializationすると、おおよそ以下の様な宣言になります。

//与えられた条件全てがtrueの場合に有効化
template<typename... Cond>
using enabler = std::enable_if_t<std::conjunction<Cond...>::value, std::nullptr_t>;

template<typename T>
class optional {

  template<typename U=T,
    enabler<
      std::is_constructible<T, U&&>,
      std::negation<
        std::is_same<std::decay_t<U>, std::in_place_t>
      >,
      std::negation<
        std::is_same<std::optional<T>, std::decay_t<U>>
      >,
      std::is_convertible<U&&, T>
    > = nullptr
  >
  constexpr optional(U&& rhs);

  template<typename U=T,
    enabler<
      std::is_constructible<T, U&&>,
      std::negation<
        std::is_same<std::decay_t<U>, std::in_place_t>
      >,
      std::negation<
        std::is_same<std::optional<T>, std::decay_t<U>>
      >,
      std::negation<
        std::is_convertible<U&&, T>
      >
    > = nullptr
  >
  explicit constexpr optional(U&& rhs);

};

もう見るのも嫌ですね。2つを書かされる差は説明してきたようにexplicitを付けるためのstd::is_convertible<U&&, T>の結果がtrueなのかfalseなのかの所だけです。

ではexplicit(bool)を使ってみれば・・・

template<typename T>
optional {

  template<typename U=T,
    enabler<
      std::is_constructible<T, U&&>,
      std::negation<
        std::is_same<std::decay_t<U>, std::in_place_t>
      >,
      std::negation<
        std::is_same<std::optional<T>, std::decay_t<U>>
      >,
    > = nullptr
  >
  explicit(!std::is_convertible_v<U&&, T>)
  constexpr optional(U&& rhs);

};

要件を記述するSFINAE部はどうしようもありませんが、記述が一つにまとまり、explicitとなる条件が分離されて見やすくなっています。

これらのように、explicit(bool)があると自分で別の型をラップするような型を作る際にコンストラクタをいくらか簡単に書くことができるようになります。
特に、optionalの例のように複雑な要件が絡む場合に強い恩恵を感じることができるかと思います。

参考文献

この記事のMarkdownソース