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

履歴 編集

関数を条件付きでexplicitにする構文を追加(C++20)

概要

C++20では、関数を条件付きでexplicitにする構文が追加された。コンストラクタと変換演算子に指定するexplicit指定子に、explicit(true)explicit(false)のようにboolに変換可能な定数式を指定する。trueに評価される値を指定することで、その関数はexplicitとなる。

template <bool Cond>
struct X {
  explicit(Cond) X() {}
};

X<false> f1() { return {}; }         // OK
//X<true> f2() { return {}; }        // コンパイルエラー!
X<false> f3() { return X<false>{}; } // OK
X<true> f4() { return X<true>{}; }   // OK

int main()
{
  X<false> x1 = {};  // OK
  //X<true> x2 = {}; // コンパイルエラー!

  X<false> x3{};     // OK
  X<true> x4{};      // OK
}

これは、関数テンプレートのコンストラクタにおいて、自身に暗黙変換可能なパラメータを受け取る場合は非explicitにするために使用できる。

コンストラクタだけではなく、型変換演算子もまた、条件付きexplicitにできる:

struct X {
  explicit(true) operator bool() const { return true; }
};

#include <type_traits>
#include <string>
#include <vector>

template <class T>
class AnyValue {
  T value_;
public:
  template <class U>
  explicit(!std::is_convertible_v<U, T>)
  AnyValue(U value) : value_(value) {}

  const T& get() const { return value_; }
};

// const char*はstd::stringに暗黙変換可能
AnyValue<std::string> f1() { return "Hello"; }   // OK
AnyValue<std::string> f2() { return {"Hello"}; } // OK

// intはstd::vector<int>に暗黙変換できないので、
// 要素数を指定するコンストラクタを呼び出すという意図を明確に伝えないといけない
//AnyValue<std::vector<int>> f3()
//{
//  return 1; // コンパイルエラー!
//}

//AnyValue<std::vector<int>> f4()
//{
//  return {1}; // コンパイルエラー!
//}
AnyValue<std::vector<int>> f5()
{
  return AnyValue<std::vector<int>>{1}; // OK
}

int main() {}

出力

この機能が必要になった背景・経緯

C++14では、std::tuplestd::pairの初期化子リストを使用した以下の初期化が不適格となっていた:

std::tuple<int, int> pixel_coordinates()
{
  return {10, -15};  // C++14でコンパイルエラー!
}

struct NonCopyable { NonCopyable(int); NonCopyable(const NonCopyable&) = delete; };
std::pair<NonCopyable, double> pmd{42, 3.14};  // C++14でコンパイルエラー!

この問題に対して、C++17では (対応が早い処理系はそれ以前から) これらのクラスのコンストラクタが、条件付きでexplicit定義されるようにした。その実装方法としてはSFINAEによって、explicitコンストラクタと非explicitコンストラクタをオーバーロードする方法が使われていた。

template <typename T1, typename T2>
struct pair {
  template <typename U1=T1, typename U2=T2,
    std::enable_if_t<
      std::is_constructible_v<T1, U1> &&
      std::is_constructible_v<T2, U2> &&
      std::is_convertible_v<U1, T1> &&
      std::is_convertible_v<U2, T2>
    , int> = 0>
  constexpr pair(U1&&, U2&&);

  template <typename U1=T1, typename U2=T2,
    std::enable_if_t<
      std::is_constructible_v<T1, U1> &&
      std::is_constructible_v<T2, U2> &&
      !(std::is_convertible_v<U1, T1> &&
        std::is_convertible_v<U2, T2>)
    , int> = 0>
  explicit constexpr pair(U1&&, U2&&);
};

SFINAEによるオーバーロードはパラメータ型を制約するために依然として必要だが、関数ボディの実装が共通であるexplicitコンストラクタと非explicitコンストラクタは、オーバーロードではなく条件による切り替えをしたかったため、条件付きで関数をexplicitにする構文が追加されることとなった。これを使用した場合、前述したstd::pairのコンストラクタは、以下のように宣言できる:

template <typename T1, typename T2>
struct pair {
  template <typename U1=T1, typename U2=T2,
    std::enable_if_t<
      std::is_constructible_v<T1, U1> &&
      std::is_constructible_v<T2, U2>
    , int> = 0>
  explicit(!std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>)
  constexpr pair(U1&&, U2&&);
};

コンセプトを使用した場合は、以下のようになる:

template <typename T1, typename T2>
struct pair {
  template <typename U1=T1, typename U2=T2>
    requires std::is_constructible_v<T1, U1> &&
      std::is_constructible_v<T2, U2>
  explicit(!std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>)
  constexpr pair(U1&&, U2&&);
};

関連項目

参照