16

C++ Rule of Threeについて多くのことを読みました。多くの人がそれを誓います。しかし、ルールが述べられている場合、ほとんどの場合、例外があることを示す「通常」、「可能性が高い」、「おそらく」などの単語が含まれています。これらの例外的なケースが何であるかについての議論はあまり見たことがありません.3つのルールが成り立たないケース、または少なくともそれを順守しても利点がないケースです。

私の質問は、私の状況が 3 つのルールの正当な例外であるかどうかです。以下で説明する状況では、明示的に定義されたコピー コンストラクターとコピー代入演算子が必要ですが、既定の (暗黙的に生成された) デストラクタは正常に機能します。これが私の状況です:

A と B の 2 つのクラスがあります。ここで問題になっているのは A です。B は A のフレンドです。A には B オブジェクトが含まれています。B には、B オブジェクトを所有する A オブジェクトを指すことを意図した A ポインターが含まれています。B は、このポインターを使用して、A オブジェクトのプライベート メンバーを操作します。B は、A コンストラクター以外でインスタンス化されることはありません。このような:

// A.h

#include "B.h"

class A
{
private:
    B b;
    int x;
public:
    friend class B;
    A( int i = 0 )
    : b( this ) {
        x = i;
    };
};

と...

// B.h

#ifndef B_H // preprocessor escape to avoid infinite #include loop
#define B_H

class A; // forward declaration

class B
{
private:
    A * ap;
    int y;
public:
    B( A * a_ptr = 0 ) {
        ap = a_ptr;
        y = 1;
    };
    void init( A * a_ptr ) {
        ap = a_ptr;
    };
    void f();
    // this method has to be defined below
    // because members of A can't be accessed here
};

#include "A.h"

void B::f() {
    ap->x += y;
    y++;
}

#endif

なぜそのようなクラスを設定するのでしょうか? 私は約束します、私には正当な理由があります。これらのクラスは、実際には、ここに含めた以上のことを行います。

残りは簡単ですよね?リソース管理もビッグスリーも問題ありません。違う!A の既定の (暗黙的な) コピー コンストラクターでは十分ではありません。これを行う場合:

A a1;
A a2(a1);

a2と同一の新しい A オブジェクトを取得します。これは、と同一であり、まだ を指していることを意味しa1ます。これは私たちが望んでいるものではありません。デフォルトのコピー コンストラクターの機能を複製し、新しい A オブジェクトを指すようにnew を設定する A のコピー コンストラクターを定義する必要があります。このコードを に追加します。a2.ba1.ba2.b.apa1A::b.apclass A

public:
    A( const A & other )
    {
        // first we duplicate the functionality of a default copy constructor
        x = other.x;
        b = other.b;
        // b.y has been copied over correctly
        // b.ap has been copied over and therefore points to 'other'
        b.init( this ); // this extra step is necessary
    };

コピー代入演算子は同じ理由で必要であり、デフォルトのコピー代入演算子の機能を複製してから を呼び出す同じプロセスを使用して実装されますb.init( this );

ただし、明示的なデストラクタは必要ありません。したがって、この状況は 3 つのルールの例外です。私は正しいですか?

4

3 に答える 3

9

「3つのルール」についてはあまり心配しないでください。ルールは盲目的に従うためのものではありません。彼らはあなたに考えさせるためにそこにいます。あなたは考えました。そして、デストラクタはそれをしないと結論付けました。だから、書かないでください。リソースをリークするデストラクタを書き忘れないようにするためのルールが存在します。

それでも、この設計は B::ap が間違っている可能性を生み出します。これは、これらが単一のクラスである場合、またはより堅牢な方法で結び付けられている場合に排除できる潜在的なバグのクラス全体です。

于 2013-03-21T21:33:33.750 に答える
4

Bと強く結合しているようAで、常にAそれを含むインスタンスを使用する必要がありますか? そして、それにはA常にBインスタンスが含まれていますか?そして、彼らは友情を介して互いの非公開メンバーにアクセスします。

したがって、なぜそれらがまったく別のクラスなのか疑問に思います。

しかし、別の理由で 2 つのクラスが必要であると仮定すると、コンストラクターとデストラクタの混乱をすべて取り除く簡単な修正方法を次に示します。

class A;
class B
{
     A* findMyA(); // replaces B::ap
};

class A : /* private */ B
{
    friend class B;
};

A* B::findMyA() { return static_cast<A*>(this); }

コンテインメントを引き続き使用し、マクロを使用してAfromBthisポインターのインスタンスを見つけることができます。offsetofしかし、それstatic_castはコンパイラを使用してポインター演算に参加させるよりも面倒です。

于 2013-03-21T20:42:09.470 に答える
2

私は@dspeyerと一緒に行きます。あなたが考え、あなたが決める。実際、誰かがすでに、通常は 3 つのルール (設計中に正しい選択をした場合) が 2 つのルールになると結論付けています: リソースをライブラリ オブジェクト (上記のスマート ポインターなど) で管理すると、通常はそれらを取り除くことができます。デストラクタ。運が良ければ、すべてを取り除き、コンパイラーに頼ってコードを生成することができます。

補足:コピーコンストラクターは、コンパイラーが生成したものを複製しません。コンパイラーがコピーコンストラクターを使用する間、その内部でコピー代入を使用します。コンストラクター本体の割り当てを取り除き、初期化リストを使用します。それはより速く、よりきれいになります。

Ben からの良い質問、良い回答 (職場の同僚を混乱させるもう 1 つのトリック) です。

于 2013-03-21T22:41:18.317 に答える