約 1 か月前に、私の新しいパーサー コンビネーター ライブラリである lexy に対する興味深いプル リクエストを受け取りました。それは、自明にコピー可能な型と、共用体を含むクラスの特別なメンバー関数に関連する一見奇妙な問題を修正しました。特別なメンバー関数と、やや驚くべき実現:
クラスが 01
であるという理由だけで クラスが実際に 18
であることを意味するものではありません または 23
:コピーできないクラスを作成できますが、それらは簡単にコピーできます。また、コピー コンストラクターが任意の量の重要な作業を実行できるが、それでも簡単にコピーできるクラスを作成できます!
説明させてください。
特別メンバー関数
クラスのデフォルト コンストラクター、コピー コンストラクター、ムーブ コンストラクター、コピー代入演算子、ムーブ代入演算子、およびデストラクターは、特別なメンバー関数と呼ばれます。ルールは複雑ですが、幸いなことに、ここでそれらを気にする必要はありません (決して気にする必要はありません)。
クラス 37
のデフォルトのコンストラクター 引数なしで呼び出すことができるコンストラクタです:
T(); // ok, no arguments
T(int i = 42, float f = 3.14); // ok, all arguments defaulted
template <typename ... Args>
T(const Args&... args); // ok, can be called with no arguments
クラス 40
のコピー コンストラクター 最初の引数が 53
型の非テンプレート (!) コンストラクターです 、 69
、 72
、または 80
、および他のすべてのパラメーター (存在する場合) にはデフォルトの引数があります。同様に、クラス 91
の移動コンストラクター 最初の引数が 105
型の非テンプレート (!) コンストラクターです 、 113
、 123
または 132
、および他のすべてのパラメーター (存在する場合) には、既定の引数があります。
T(const T& other); // traditional copy constructor
T(T&& other); // traditional move constructor
T(const T& other, int i = 42); // copy constructor, second argument defaulted
T(T& other); // copy constructor
template <typename Arg>
T(Arg&& other); // not a copy/move constructor, templated
クラス 145
のコピー代入演算子 テンプレート化されていない (!) 154
唯一の引数が 169
型のオーバーロード 、 173
、 188
、または 199
同様に、クラス 208
のムーブ代入演算子 テンプレート化されていない (!) 210
唯一の引数が 221
型のオーバーロード 、 234
、 248
、または 258
戻り値の型またはメンバー関数の cv/ref 修飾子は重要ではないことに注意してください。
T& operator=(const T& other); // traditional copy assignment
T& operator=(T&& other); // traditional move assignment
int operator=(const T& other) volatile &&; // copy assignment
template <typename Arg>
T& operator=(Arg&& other); // not a copy/move assignment, templated
デストラクタは 260
を持つ奇妙なメンバー関数です .
これらのルールを覚えておいてください。後で重要になります。
特殊メンバー関数の型特性
各特別なメンバー関数には型特性 272
があります これにより、その存在を照会できます。そのため、クラスにコピー コンストラクターがある場合、281
297
です .
ただし、これはそれらの特性が実際に行うことではありません!
特性は、式が整形式かどうかを照会します:
300
313
かどうか調べます 整形式です。327
331
かどうか調べます 整形式です。340
358
かどうかを調べます 整形式です。364
376
かどうかを調べます 整形式です。386
395
かどうかを調べます 整形式です。406
413
かどうかを調べます 整形式です。
これは、型特性が「クラスにはこの特別なメンバー関数があるか?」という仮説とは異なる結果を報告できることを意味します。 trait.まず、アクセス指定子を無視します:プライベート コピー コンストラクターがある場合、422
433
です .しかし、状況によってはさらに微妙な違いもあります:
struct weird
{
weird& operator=(const volatile weird&) = delete; // (1)
template <int Dummy = 0>
weird& operator=(const weird&) // (2)
{
return *this;
}
};
static_assert(std::is_copy_assignable_v<weird>); // ok
weird w;
w = w; // invokes (2)
ゴッドボルトリンク
演算子のオーバーロード (1) は削除されたコピー代入演算子です。演算子のオーバーロード (2) はテンプレートであるため、代入演算子とは見なされません。ただし、449
のオーバーロード解決 「コピー代入演算子」が正確に何であるかは気にせず、通常どおり機能します。そのため、テンプレート化されたオーバーロードを見つけます (452
を取るものよりも一致します)。 )、技術的にはコピー代入演算子がありませんが、喜んでオブジェクトを「コピー代入」します。これも 462
です。 チェックするので、アサーションはパスします。
何かが特別なメンバー関数であるかどうかを判断するルールと、実際に呼び出されるコンストラクター/代入演算子を判断するルールはまったく異なります!
何かが特別なメンバー関数であるかどうかを判断するには、上記の署名を持つメンバーを探します。何が呼び出されたかを判断するには、通常のオーバーロード解決を行います。
オーバーロードの解決を行う型の特徴により、正しい結果が得られることに注意してください。 475
のようなもの 対応する関数がどこかにあるかどうかではなく、そのように見える何かを呼び出すことができるかどうかを照会したいので、あまり役に立ちません.
自明な特殊メンバー関数
特別なメンバー関数は自明です (トピックではありません。実際のメンバー関数はこのプロパティを持つことができます)。ユーザーが提供しない場合 (つまり、485
を使用します)、それらは自明です。 または暗黙的に生成される)、およびすべてのメンバー/基本クラスの対応する関数も自明です。 .
struct foo
{
int a;
float f;
foo() = default; // trivial
// implicitly declared copy constructor is trivial
~foo() {} // not-trivial, user provided
};
自明な特殊メンバー関数の型特性
上記の 6 つの型特性はそれぞれ 504
にも含まれています。 フレーバーです。繰り返しますが、型が持っているかどうかはチェックしません 些細な特別なメンバー関数ですが、対応する式が呼び出すかどうか 些細な機能のみ。
struct weird
{
weird& operator=(const volatile weird&) = delete; // (1)
template <int Dummy = 0>
weird& operator=(const weird&) // (2)
{
return *this;
}
};
static_assert(std::is_copy_assignable_v<weird>); // ok
// not ok, (2) is non-trivial
static_assert(std::is_trivially_copy_assignable_v<weird>);
ゴッドボルトリンク
繰り返しますが、これは便利です:519
かどうかを確認したい クラスに重要な関数があるかどうかではなく、重要な関数を呼び出します。
522
これで 531
にたどり着きました 、これは 548
とはまったく異なることを行います !
554
565
かどうかを調べます 自明にコピー可能な型です (duh)。自明にコピー可能な型は、基本型またはクラスのいずれかです。
条件 1 は単純である必要があります。型のデストラクタは何も実行してはなりません。条件 2 は、型に特別なメンバー関数がある場合、それは自明でなければならないことを示しています。最後に、条件 3 は、オブジェクトを再配置する何らかの方法が必要であることを示しています。ある場所から別の場所へ。完全に動かせない型は、簡単にコピーできません。
579
に注意してください 584
の可能性があります 、しかし 595
608
の可能性があります :610
自明にコピー可能にするためにコピー構築可能である必要はありません 624
636
の可能性があります .
それをすべて手に入れましたか?面白くなってきているからです。
上記の定義に基づいて、 644
を実装したくなるかもしれません 次のように:
template <typename T>
constexpr bool is_trivially_copyable_v
// condition 1
= std::is_trivially_destructible_v<T>
// condition 2
&& (!std::is_copy_constructible_v<T> || std::is_trivially_copy_constructible_v<T>)
&& (!std::is_move_constructible_v<T> || std::is_trivially_move_constructible_v<T>)
&& (!std::is_copy_assignable_v<T> || std::is_trivially_copy_assignable_v<T>)
&& (!std::is_move_assignable_v<T> || std::is_trivially_move_assignable_v<T>)
// condition 3
&& (std::is_copy_constructible_v<T> || std::is_move_constructible_v<T>
|| std::is_copy_assignable_v<T> || std::is_move_assignable_v<T>);
実際、これは基本的に clang が 651
を実装する方法です。
しかし、この実装は間違っています!
666
とは異なります 、 671
しません オーバーロード解決を使用して式をチェックします。実際に先に進み、特別なメンバー関数の存在を探します!
これにより、おかしな状況が発生する可能性があります:
struct weird
{
weird() = default;
weird(const weird&) = default;
weird(weird&&) = default;
~weird() = default;
weird& operator=(const volatile weird&) = delete; // (1)
template <int Dummy = 0>
weird& operator=(const weird&) // (2)
{
return *this;
}
};
static_assert(std::is_copy_assignable_v<weird>); // (a)
static_assert(!std::is_trivially_copy_assignable_v<weird>); // (b)
static_assert(std::is_trivially_copyable_v<weird>); // (c)
ゴッドボルトリンク
オーバーロードの解決がテンプレート化されたオーバーロード (2) を見つけるため、アサーション (a) は成功します。
ただし、アサーション (c) はパスします (clang を使用しない場合):680
オーバーロードの解決を行わずに特別なメンバー関数をチェックします。削除されていない単純なデストラクタとコピー/移動コンストラクタ、削除されたコピー代入演算子があります。そのため、単純にコピー可能です。
実際のコピー割り当て 696
自明でない任意のコードを呼び出す可能性があることは問題ではありません。型は依然として自明にコピー可能です!
型がコピー代入可能で自明にコピー可能であるという理由だけで、他のすべての特別なメンバー関数と同様に、その型が自明にコピー代入可能であるとは限りません。
わかりました、それは少し奇妙です.しかし、確かに 702
のような型を書く人は誰もいません. 重要な型特性は 716
のいずれかです または 723
のいずれか 状況に応じて 2 つの混合ではありません。
…何が来るか知ってる?
734
Microsoft の 749
として知られています。 標準では絶対に 754
が必要な場合があります 時々 766
状況に応じて!
自明なコピー可能性と自明な関数の呼び出し
標準では、型は 775
である必要があります 次の状況で:
- 単純にコピー可能な関数呼び出しからオブジェクトを渡したり返したりする場合、最適化のためにレジスタで渡したり返したりすることができます。
788
自明にコピー可能な型でのみ使用する必要があり、動作が保証されています。798
自明にコピー可能な型でのみ使用する必要があります。
一方、標準では、オーバーロードの解決が単純な特別なメンバー関数 (802
) のみを呼び出すことを要求しています。 )
- 特別なメンバー関数のデフォルトの実装が自明かどうかを判断するとき
- ユニオンのアクティブなメンバーが直接割り当てによって変更された場合
- 共用体に削除されていない特別なメンバー関数があるかどうかを判断するとき。
ユニオンのケースは興味深い:ユニオンのコピーはオブジェクト表現をコピーするために定義されており、これは基本的に 816
.821
ただし、オーバーロードの解決ですべてのバリアントの自明なコピー コンストラクターが見つかった場合、共用体には削除されていないコピー コンストラクターしかありません。これは、自明なコピー可能な型に対して存在することが保証されていません!
これは、 839
を入れるだけでは不十分であることを意味します ユニオンに入力する場合、実際には 849
である必要があります – 実際のコピー操作には 851
しか必要ありませんが :
// As above.
struct weird
{
weird() = default;
weird(const weird&) = default;
weird(weird&&) = default;
~weird() = default;
weird& operator=(const volatile weird&) = delete;
template <int Dummy = 0>
weird& operator=(const weird&)
{
return *this;
}
};
static_assert(std::is_copy_assignable_v<weird>);
static_assert(!std::is_trivially_copy_assignable_v<weird>);
static_assert(std::is_trivially_copyable_v<weird>);
union weird_union
{
int i;
weird w;
} u;
u = u; // error: weird_union has deleted copy assignment
ゴッドボルトリンク
覚えておいてください:864
より一般的には 877
として知られています .これがまさに lexy の最初のバグの原因でした。
すべてを理解した後、私が書いたツイートをそのままにしておきます:
(標準の動作は少し驚くべきものです。MSVC の 881
自明にコピー割り当て可能ではなく、clang は 899
を行いません 正しく)
結論
自明な特殊メンバー関数に関する型特性には、次の 2 つの異なるカテゴリがあります:901
と 915
.最初のカテゴリはオーバーロード解決を行い、いくつかの式を評価し、呼び出された関数が自明かどうかを判断します。2 番目のカテゴリは、クラスが特定のシグネチャに一致する関数を定義しているかどうかを調べます。
これにより、基本的に互換性がなくなります。
ほとんどの場合、実際に必要な型特性は最初のカテゴリにあります。実際にコードに式を入力し、それが自明かどうかを確認したい場合は、それらを使用して特別なメンバー関数を制約したり、自明なものと自明でないものを選択したりしますユニオンベースの実装
924
930
を呼び出す必要がある場合にのみ使用してください または 940
(または上に構築された関数)。特に、「すべての特別なメンバー関数は自明です」の省略形として使用しないでください。実際にはそうではないためです!
常に覚えておいてください:タイプは 956
である可能性があります 960
でなくても または 971
コピー コンストラクターが削除された :types は自明にコピー可能であり、オーバーロード解決がコピー中に非自明なコンストラクターを選択する型は、自明なコピー コンストラクターを持つことができます。