[C++]メンバ型のトリビアル性を継承するロストテクノロジー

std::optionalstd::variantは保持する型がトリビアルな型であれば、そのトリビアル性を継承することが規格によって求められており、その実装には非常に難解なテクニックが使用されます。しかし、C++20以降、このテクニックは過去のものとなり忘れ去られていく事でしょう。この記事はそんなロストテクノロジーの記録です。

メンバ型のトリビアル性を継承、とは?

テンプレートパラメータで指定された型の値をメンバとして保持するときに、そのテンプレートパラメータの型のトリビアル性を継承する事です。

template<typename T>
struct wrap {
  T t;
};

template<typename T>
void f(wrap<T>) {
  // 要素型Tがトリビアルであれば
  static_assert(std::is_trivial_v<T>);
  // wrap<T>もトリビアルとなってほしい
  static_assert(std::is_trivial_v<wrap<T>>);
}

トリビアルというのは、クラスの特殊メンバ関数がユーザーによって定義されていないことを言います(単純には)。これによって、trivially copyableならばmemcpyできるようになるとか、trivially destructibleならばデストラクタ呼び出しを省略できる、などの保証が得られます。

上記のwrap<T>型のように単純な型であれば単純にメンバとして保持しただけでも継承していますが、std::optionalのように複雑な型ではそうは行きません。しかしそれをなんとかする方法がちゃんと存在しています。

optional<T>簡易実装

この記事ではoptionalの簡易実装によってメンバ型のトリビアル性継承がどのように行われるのかを見ていきますので、ここでベースとなる簡易実装rev1を書いておきます。

template<typename T>
class my_optional {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

public:

  // デフォルトコンストラクタ
  constexpr my_optional() 
    : dummy{}
    , has_value(false)
  {}

  // 値を受け取るコンストラクタ
  template<typename U=T>
  constexpr my_optional(U&& v)
    : data(std::forward<U>(v))
    , has_value(true)
  {}

  // コピーコンストラクタ
  my_optional(const my_optional& that)
    : dummy{}
    , has_value(that.has_value)
  {
    if (that.has_value) {
      new (&this->data) T(that.data);
    }
  }

  // ムーブコンストラクタ
  my_optional(my_optional&& that)
    : dummy{}
    , has_value(that.has_value)
  {
    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }
  }

  // コピー代入演算子
  my_optional& operator=(const my_optional& that) {
    auto copy = that;
    *this = std::move(copy);
    
    return *this;
  }

  // ムーブ代入演算子
  my_optional& operator=(my_optional&& that) {
    if (this->has_value) {
      this->data.~T();
    }

    this->has_value = that.has_value;

    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }

    return *this;
  }

  // デストラクタ
  ~my_optional() {
    if (has_value) {
      this->data.~T();
    }
  }
};

この実装はとりあえずoptionalっぽい働きはします。C++11で制限解除された共用体はそのメンバ型が非トリビアルな特殊メンバ関数を持つとき、対応する特殊メンバ関数deleteされます。そのため、それをラップする外側の型はそれを書いておく必要があります。optionalは遅延構築や任意タイミングでの無効値への切り替えが可能であり、それを実現するためには共用体を利用するのが最短でしょう。なお、状態を変化させるのは他のメンバ関数や代入演算子で行いますが、ここではそれは重要ではないので省略します。また、noexceptについては考えないことにします。

デストラク

簡易実装rev1はデストラクタがトリビアルではありません。Ttrivially destructibleであるならばデストラクタ呼び出しは省略できるので、my_optionalのデストラクタもトリビアルに出来そうです。そしてそれは、C++17の世界でmy_optionalconstexprとなるための必要十分条件です。

デストラクタのトリビアル性継承は要するに、Tトリビアルデストラクタを持つ場合にdefaultで、そうではない場合に独自定義、という風に分岐してやればいいのです。それはクラステンプレートの部分特殊化を用いて、次のように実装できます。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

  // デストラクタは常に非トリビアルでdeleteされているので定義する
  ~optional_storage() {
    if (has_value) {
      this->data.~T();
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

  // デストラクタはトリビアルであり常にdeleteされないので、宣言すらいらない
};

template<typename T>
class my_optional : private optional_storage<T> {
public:

  // 他略

  // デストラクタ、この宣言も実はいらない
  ~my_optional() = default;
};

optional_storage<T>というクラスにデータを保持する部分を移管し、optional_storage<T>Ttrivially destructibleである場合とない場合でテンプレートの部分特殊化によって実装を切り替えます。そしてその実装では、Ttrivially destructibleである場合はデストラクタはトリビアルに定義され(ユーザー定義されず)、Ttrivially destructibleでない場合に引き続きユーザー定義されます。これらの選択は与えられた型Tによって自動的に行われ、my_optional<T>Ttrivially destructible性を継承します。

int main() {
  // パスする
  static_assert(std::is_trivially_destructible_v<my_optional<int>>);
  static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false);
}

簡易実装rev2は次のようになりました。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {
  bool has_value = false;
  union {
    char dummy;
    T data;
  };

  // デストラクタは常に非トリビアルでdeleteされているので定義する
  ~optional_storage() {
    if (has_value) {
      this->data.~T();
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {
  bool has_value = false;
  union {
    char dummy;
    T data;
  };

  // デストラクタはトリビアルであり常にdeleteされないので、宣言すらいらない
};

template<typename T>
class my_optional : private optional_storage<T> {
public:

  // デフォルトコンストラクタ
  constexpr my_optional() 
    : has_value(false)
    , dummy{}
  {}

  // 値を受け取るコンストラクタ
  template<typename U=T>
  constexpr my_optional(U&& v)
    : has_value(true)
    , data(std::forward<U>(v))
  {}

  // コピーコンストラクタ
  my_optional(const my_optional& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(that.data);
    }
  }

  // ムーブコンストラクタ
  my_optional(my_optional&& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }
  }

  // コピー代入演算子
  my_optional& operator=(const my_optional& that) {
    auto copy = that;
    *this = std::move(copy);
    
    return *this;
  }

  // ムーブ代入演算子
  my_optional& operator=(my_optional&& that) {
    if (this->has_value) {
      this->data.~T();
    }

    this->has_value = that.has_value;

    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }

    return *this;
  }
};

コピー/ムーブコンストラク

コピー/ムーブコンストラクタをトリビアルに定義するとは、先程のデストラクタのようにTでのそれがトリビアルならばmy_optionalでのそれもトリビアルとなるようにすればいいのです。が、冷静に考えてみると、すでにデストラクタのトリビアル性で分岐している所にコピーコンストラクタのそれでさらに分岐し、さらにムーブコンストラクタでも・・・となって組合せ爆発のようになることがわかるでしょう。じゃあいい方法が・・・ないので愚直に書きましょう。

ただ、そのような分岐を1つのクラスにまとめようとすると組合せ爆発で死ぬのは想像が付くので、特殊メンバ関数一つに対して1つのクラスが必要で、その1つのクラスにはdefaultによるトリビアルな定義をするものと自前定義するものの2つの特殊化が必要になりそうです。
もう少しよくよく考えてみると、Tのある特殊メンバ関数トリビアルであるとき、基底となるoptional_storageでもそれはトリビアルに定義できるはずなので、そこで定義されたそれを活用すればトリビアルケースの定義を省略出来る事に気づけます(私は気づきませんでしたが)。

コピーコンストラクタだけで見てみると、次のようになります。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;
  
  constexpr optional_storage()
    : dummy{}
    , has_value(false)
  {}
  
  template<typename... Args>
  constexpr optional_storage(Args&&... arg)
    : data(std::forward<Args>(arg)...)
    , has_value(true)
  {}
  
  // 定義できればトリビアル、そうでないなら暗黙delete
  optional_storage(const optional_storage&) = default;
  optional_storage(optional_storage&&) = default;
  optional_storage& operator=(const optional_storage&) = default;
  optional_storage& operator=(optional_storage&&) = default;

  ~optional_storage() {
    if (has_value) {
      this->data.~T();
    }
  }
  
  template<typename... Args>
  void construct(Args&&... arg) {
    new (&this->data) T(std::forward<Args>(arg)...);
    has_value = true;
  }
  
  template<typename Self>
  void construct_from(Self&& that) {
    if (that.has_value) {
      // thatの値カテゴリを伝播する
      construct(std::forward<Self>(that).data);
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

  constexpr optional_storage()
    : dummy{}
    , has_value(false)
  {}
  
  template<typename... Args>
  constexpr optional_storage(Args&&... arg)
    : data(std::forward<Args>(arg)...)
    , has_value(true)
  {}
  
  // 定義できればトリビアル、そうでないなら暗黙delete
  optional_storage(const optional_storage&) = default;
  optional_storage(optional_storage&&) = default;
  optional_storage& operator=(const optional_storage&) = default;
  optional_storage& operator=(optional_storage&&) = default;
  
  template<typename... Args>
  void construct(Args&&... arg) {
    new (&this->data) T(std::forward<Args>(arg)...);
    has_value = true;
  }
  
  template<typename Self>
  void construct_from(Self&& that) {
    if (that.has_value) {
      // thatの値カテゴリを伝播する
      construct(std::forward<Self>(that).data);
    }
  }
};

template<typename T>
struct enable_copy_ctor : optional_storage<T> {
  using base = optional_storage<T>;

  // ユーザー定義コピーコンストラクタ
  enable_copy_ctor(const enable_copy_ctor& that)
    : base()
  {
    this->construct_from(static_cast<const base&>(that));
  }

  // 他のは全部基底のものか上で定義されるものに頼る!
  enable_copy_ctor() = default;
  enable_copy_ctor(enable_copy_ctor&&) = default;
  enable_copy_ctor& operator=(const enable_copy_ctor&) = default;
  enable_copy_ctor& operator=(enable_copy_ctor&&) = default;

};

template<typename T>
using check_copy_ctor = std::conditional_t<
  std::is_trivially_copy_constructible_v<T>,
  optional_storage<T>,
  enable_copy_ctor<T>
>;

template<typename T>
class my_optional : private check_copy_ctor<T> {
public:
  // 他略

  // コピーコンストラクタ
  // copy_ctor_enabler<T>のコピーコンストラクタを利用する
  my_optional(const my_optional& that) = default;
};

C++11以降の共用体は内包する型の特殊メンバ関数トリビアルでないならば、対応する自身の特殊メンバ関数が暗黙deleteされます。従って、optional_storageではデストラクタ以外をとりあえず全部default定義しておけば、トリビアルの時だけは定義されていることになります。

それを利用し、Ttrivially copyableの時だけ、my_optionalからoptional_storageに至るクラス階層にコピーコンストラクタをユーザー定義するクラスを追加し、そうでなければoptional_storageを直接利用します。すると、最上位my_optionalクラスからはその基底クラスのコピーコンストラクタは常に何かしら定義されているように見えるため、my_optionalのコピーコンストラクタはdefaultで定義する事ができます。

派生クラスのコンストラクタ初期化子リストからは最基底のoptional_storageのメンバは触れませんので、optional_storageにはコンストラクタが必要です。また、フラグの管理とか構築周りのことを共通化するためにoptional_storageconstruct()/construct_from()関数を追加しておきます。

同じようにムーブコンストラクタを定義しましょう。

template<typename T>
struct enable_move_ctor : check_copy_ctor<T> {
  using base = check_copy_ctor<T>
  
  // ユーザー定義ムーブコンストラクタ
  enable_move_ctor(enable_move_ctor&& that)
    : base()
  {
    this->construct_from(static_cast<base&&>(that));
  }

  // コピーコンストラクタはenable_copy_ctorで定義されるか
  // optional_storageでトリビアルに定義される
  enable_move_ctor(const enable_move_ctor&) = default;

  enable_move_ctor() = default;
  enable_move_ctor& operator=(const enable_move_ctor&) = default;
  enable_move_ctor& operator=(enable_move_ctor&&) = default;
};

template<typename T>
using check_move_ctor = std::conditional_t<
  std::is_trivially_move_constructible_v<T>,
  check_copy_ctor<T>,
  enable_move_ctor<T>
>;

template<typename T>
class my_optional : private check_move_ctor<T> {
public:
  // 他略

  // ムーブコンストラクタ
  my_optional(my_optional&&) = default;
};

my_optionalcheck_copy_ctorの間に、さっきと同じようなものを挿入してやるだけです、簡単ですね・・・

int main() {
  // パスする
  static_assert(std::is_trivially_destructible_v<my_optional<int>>);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_move_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_move_constructible_v<my_optional<std::string>> == false);
}

intは当然トリビアルなクラスでありstd::stringは全ての特殊メンバ関数がそうではないので、このstatic_assert群によってちゃんとトリビアル性が伝播されている事がわかります。

代入演算子

残ったのはコピー/ムーブ代入演算子です。これは特別な事をする必要はほぼなく、コンストラクタの時と同様のアプローチによって実装できます。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {

  // 中略

  template<typename Self>
  void asign_from(Self&& that) {
    if (that.has_value) {
      if (this->has_value) {
        this->data = std::forward<Self>(that).data;
      } else {
        this->construct(std::forward<Self>(that).data);
      }
    } else {
      this->reset();
    }
  }
  
  void reset() {
    if (this->has_value) {
      this->data.~T();
      this->has_value = false;
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {

  // 中略

  template<typename Self>
  void asign_from(Self&& that) {
    if (that.has_value) {
      if (this->has_value) {
        this->data = std::forward<Self>(that).data;
      } else {
        this->construct(std::forward<Self>(that).data);
      }
    } else {
      this->reset();
    }
  }
  
  void reset() {
    this->has_value = false;
  }
};

// 中略

template<typename T>
struct enable_copy_asign : check_move_ctor<T> {
  using base = check_move_ctor<T>;
  
  // ユーザー定義コピー代入演算子
  enable_copy_asign& operator=(const enable_copy_asign& that) {
    this->asign_from(static_cast<const base&>(that));
  }

  enable_copy_asign() = default;
  enable_copy_asign(const enable_copy_asign&) = default;
  enable_copy_asign(enable_copy_asign&&) = default;
  enable_copy_asign& operator=(enable_copy_asign&&) = default;
};

template<typename T>
using check_copy_asign = std::conditional_t<
  std::is_trivially_copy_assignable_v<T>,
  check_move_ctor<T>,
  enable_copy_asign<T>
>;

template<typename T>
class my_optional : private check_copy_asign<T> {
public:
  // 他略

  // コピー代入演算子
  my_optional& operator=(const my_optional&) = default;
};

代入演算子では自身の状態を一度無効化する必要がありますが、その処理はTのデストラクタがトリビアルであるかによって変化しますので、代入に伴うあれこれと共にoptional_storageに実装しておきます(asign_from()/reset())。

それを用いてenable_~_asignクラスで代入演算子を実装します。まあ、難しいところはないですね(とてもめんどくさいですね・・・)。

ムーブ代入演算子も同じように実装できます。

template<typename T>
struct enable_move_asign : check_copy_asign<T> {
  using base = check_copy_asign<T>;
  
  // ユーザー定義ムーブ代入演算子
  enable_move_asign& operator=(enable_move_asign&& that) {
    this->asign_from(static_cast<base&&>(that));
  }

  enable_move_asign() = default;
  enable_move_asign(const enable_move_asign&) = default;
  enable_move_asign(enable_move_asign&&) = default;
  enable_move_asign& operator=(const enable_move_asign&) = default;
};

template<typename T>
using check_move_asign = std::conditional_t<
  std::is_trivially_move_assignable_v<T>,
  check_copy_asign<T>,
  enable_move_asign<T>
>;

template<typename T>
class my_optional : private check_move_asign<T> {
public:
  // 他略

  // ムーブ代入演算子
  my_optional& operator=(my_optional&&) = default;
};

やることは同じです。これによってほぼ全ての特殊メンバ関数トリビアル性継承を実装することができました・・・

int main() {
  // 全てトリビアル
  static_assert(std::is_trivially_destructible_v<my_optional<int>>);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_move_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_copy_assignable_v<my_optional<int>>);
  static_assert(std::is_trivially_move_assignable_v<my_optional<int>>);
    
  // 全て非トリビアル
  static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_move_constructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_copy_assignable_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_move_assignable_v<my_optional<std::string>> == false);

  // しかしユーザー定義されている
  static_assert(std::is_destructible_v<my_optional<std::string>>);
  static_assert(std::is_copy_constructible_v<my_optional<std::string>>);
  static_assert(std::is_move_constructible_v<my_optional<std::string>>);
  static_assert(std::is_copy_assignable_v<my_optional<std::string>>);
  static_assert(std::is_move_assignable_v<my_optional<std::string>>);
}

確かに、トリビアル性を継承しつつそうでない場合はユーザー定義、というようになっています。

実はもう少し厳密にやると、そもそもTがコピー可能でない場合に適切にdeleteするとかのハンドルが必要となりますが、主題ではないのでここではやりません。

このような複雑怪奇なテクニックはしかし、std::optionalstd::variantの実装で実際に使用されています。少なくともGCC/MSVCの実装はこうなっているはずです(MSVCは将来的に変更するかもしれませんが)。そして、std::expectedなど類似のクラスでも同じ事をする必要が出てくるでしょう。

※ このあたりを書くにあたってはMSVCの実装(<optional>と、xsmf_control.h)を大変参考にしています。特に、xsmf_control.hにはこのテクニックが一般化されまとまっていて、MSVCのoptional/variantはどちらも同じものを使用しています。これはある程度TMPがわかればなんとか読めるので、気になった人はそちらを参照してください。

階層構造

check_xxxxxみたいなエイリアステンプレートは、xxxxxに対応する特殊メンバ関数トリビアルでない場合にユーザー定義する層を挿入し、そうでないならスキップします。したがって、intのような全トリビアルなクラスでは階層は最小になります。

  • my_optional<int>
    • optional_storage<int>

一方、std::stringのように全部トリビアルではないクラスではフルで挿入されることになります。

  • my_optional<std::string>
    • enable_move_asign<std::string>
      • enable_copy_asign<std::string>
        • enable_move_ctor<std::string>
          • enable_copy_ctor<std::string>
            • optional_storage<std::string>

例えばムーブだけトリビアルでないような型(move_non_trivial)なら

  • my_optional<move_non_trivial>
    • enable_move_asign<move_non_trivial>
      • enable_move_ctor<move_non_trivial>
        • optional_storage<move_non_trivial>

のようなクラス階層になります。

一部のデバッガでは、このようなクラス階層を直接観測することができます(VSのデバッガだと多分途中が省略されるので見られない気がします)。あるいは観測してなんだこれ?と思ったことがあるかもしれません。

デフォルトコンストラク

optionalはその実装の都合上、デフォルトコンストラクタをトリビアルにすることができません。そのためoptional以外を例にすると、次のように書くことで要素型のtrivially default constructible性を継承できます。

template<typename T>
class wrap {
  T t;  // 初期化しない

public:
  wrap() = default;
};

他のコンストラクタが存在するとデフォルトコンストラクタは暗黙deleteされるため、defaultで書いておきます。この時、メンバに持っているTのオブジェクトに対してデフォルトメンバ初期化してしまうとトリビアルにならないので注意が必要です。

int main() {
  static_assert(std::is_trivially_default_constructible_v<wrap<int>>);  // パスする
}

またおそらく、このような単純な型ではその他の部分のトリビアル性継承時にも先程までのような謎のテクニックを駆使する必要はないはずです。

C++20 Conditionally Trivial Special Member Functions

C++20ではコンセプトが導入され、それを利用したConditionally Trivial Special Member Functionsという機能が追加されました。これはまさに、ここまで見てきた事をコンセプトによって簡易に実現するための機能です。

これによって、my_optional実装は次のようになります。

template<typename T>
class my_optional {
  bool has_value = false;
  union {
    char dummy;
    T data;
  };

public:

  // デフォルトコンストラクタ
  constexpr my_optional() 
    : has_value(false)
    , dummy{}
  {}

  // 値を受け取るコンストラクタ
  template<typename U=T>
  constexpr my_optional(U&& v)
    : has_value(true)
    , data(std::forward<U>(v))
  {}

  // トリビアルに定義できるならそうする
  my_optional(const my_optional& that) requires std::is_trivially_copyable_v<T> = default;
  my_optional(my_optional&& that) requires std::is_trivially_movable_v<T> = default;
  my_optional& operator=(const my_optional& that) requires std::is_trivially_copy_assignable_v<T> = default;
  my_optional& operator=(my_optional&& that) requires std::is_trivially_move_assignable<T> = default;
  ~my_optional() requires std::is_trivially_destructible_v<T> = default;


  // そうでない場合はユーザー定義する

  my_optional(const my_optional& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(that.data);
    }
  }

  my_optional(my_optional&& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }
  }

  my_optional& operator=(const my_optional& that) {
    auto copy = that;
    *this = std::move(copy);
    
    return *this;
  }

  my_optional& operator=(my_optional&& that) {
    if (that.has_value) {
      if (this->has_value) {
        this->data = std::move(that.data);
      } else {
        new (&this->data) T(std::move(that.data));
      }
    } else {
      this->reset();
    }

    return *this;
  }

  ~my_optional() {
    this->reset();
  }
  
  // reset()の定義も同様の記法で分岐できる

  void reset() requires std::is_trivially_destructible_v<T> {
    this->has_value = false;
  }

  void reset() {
    if (this->has_value) {
      this->data.~T();
    }
    this->has_value = false;
  }
};

defaultな特殊メンバ関数に対してrequiresによる制約を付加する事で、テンプレートパラメータの性質によって定義するしないを分岐することができ、100行以上も謎のコードを削減することができました・・・

ここでは、オーバーロード解決時の制約式による半順序に基づいて、特殊メンバ関数定義にも制約によって順序が付けられ、最も制約されている(かつそれを満たしている)1つだけが資格のある(eligible)特殊メンバ関数として定義され、それ以外はdeleteされます。

この場合、my_optionaldefaultな特殊メンバ関数定義はis_trivially_~によって制約されており、Tの対応する特殊メンバ関数トリビアルである時my_optionalの対応する特殊メンバ関数トリビアルな方が選択され、ユーザー定義のものは無制約なのでdeleteされます。逆に、Tの対応する特殊メンバ関数トリビアルではない時、制約を満たさないことからdefaultのものがdeleteされ、結果的に適切な一つだけが定義されています。

先ほどまで書いていたものすごく労力のかかった意味のわからないコードはこれによって不要になります。このConditionally Trivial Special Member Functionsという機能がいかに強力で素晴らしく、どれほどマイナーなのかがわかるでしょう!

そしてC++20以降、あのようなテクニックは忘れ去られていく事でしょう。この記事は、失われいく謎のテクニックを後世に伝えるとともに、理解しづらいConditionally Trivial Special Member Functionsという機能の解説を試みるものでした・・・

なぜにトリビアル

長いので分けました。そもそもなんでそこまでしてトリビアル性にこだわるのか?という事を書いています。

参考文献

この記事のMarkdownソース