自明にコピー可能であることは自明にコピー構築可能であることを意味しない

約 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 型の非テンプレート (!) コンストラクターです 、 6972 、または 80 、および他のすべてのパラメーター (存在する場合) にはデフォルトの引数があります。同様に、クラス 91 の移動コンストラクター 最初の引数が 105 型の非テンプレート (!) コンストラクターです 、 113123 または 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 型のオーバーロード 、 173188 、または 199 同様に、クラス 208 のムーブ代入演算子 テンプレート化されていない (!) 210 唯一の引数が 221 型のオーバーロード 、 234248 、または 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 つの異なるカテゴリがあります:901915 .最初のカテゴリはオーバーロード解決を行い、いくつかの式を評価し、呼び出された関数が自明かどうかを判断します。2 番目のカテゴリは、クラスが特定のシグネチャに一致する関数を定義しているかどうかを調べます。

    これにより、基本的に互換性がなくなります。

    ほとんどの場合、実際に必要な型特性は最初のカテゴリにあります。実際にコードに式を入力し、それが自明かどうかを確認したい場合は、それらを使用して特別なメンバー関数を制約したり、自明なものと自明でないものを選択したりしますユニオンベースの実装

    924 930 を呼び出す必要がある場合にのみ使用してください または 940 (または上に構築された関数)。特に、「すべての特別なメンバー関数は自明です」の省略形として使用しないでください。実際にはそうではないためです!

    常に覚えておいてください:タイプは 956 である可能性があります 960 でなくても または 971 コピー コンストラクターが削除された :types は自明にコピー可能であり、オーバーロード解決がコピー中に非自明なコンストラクターを選択する型は、自明なコピー コンストラクターを持つことができます。