※この内容はC++20から利用可能になる予定の情報であり、一部の内容が変更される可能性があります。
explicit(bool)
指定子
従来コンストラクタと変換関数に付けることのできていたexplicit
指定子は
、C++20よりexplicit(expr)
という形のものに置き換えられます。
かっこの中のexpr
に指定出来るのはbool
に変換可能な定数式です。
そして、そのexpr
がtrue
となる場合はそのコンストラクタ(変換関数)は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 }
ご覧のように、包む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 }
しかしこれにも問題があります。
auto f() -> unit<int> { return {128}; //compile error! } int main() { unit<int> u1 = {10}; //ng unit<int> u2 = 10; //ng }
はい、今度は非explicitコンストラクタを持つ型を入れた時に意図しないコンパイルエラーが多発します。すべてunit
型のコンストラクタがexplicitであるためです。
(これがまさに、C++17においてN4387で解決されたstd::tuple
、std::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; };
少し見辛い所はありますがexplicit(bool)
に暗黙変換可能かどうかを判定する式を渡してやることで、一つのコンストラクタの定義だけで先ほどと同じ効果を得られます。
もはや訳の分からないSFINAEによる分岐は必要ありません。
explicit(bool)
はおそらくこの問題のためだけに導入されました。ライブラリで複雑な型を書く場合にはありがたみを感じることができるでしょう・・・
ところで、STLにはこのように内部に任意の型のオブジェクトを保持するようなクラスがいくつかあります。std::tuple
やstd::pair
、std::optional
がその代表です。
std::pair
、std::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
の例のように複雑な要件が絡む場合に強い恩恵を感じることができるかと思います。