16

我已经阅读了很多关于 C++规则三的内容。许多人对此发誓。但是当规定规则时,它几乎总是包含“通常”、“可能”或“可能”这样的词,表示存在例外情况。我还没有看到太多关于这些例外情况可能是什么的讨论——三法则不成立的情况,或者至少坚持三法则没有任何优势的情况。

我的问题是我的情况是否是三原则的合法例外。我相信在我下面描述的情况下,显式定义的复制构造函数和复制赋值运算符是必要的,但默认的(隐式生成的)析构函数可以正常工作。这是我的情况:

我有两个类,A 和 B。这里有问题的是 A。B 是 A 的朋友。A 包含一个 B 对象。B 包含一个 A 指针,该指针旨在指向拥有 B 对象的 A 对象。B 使用此指针来操作 A 对象的私有成员。除了在 A 构造函数中之外,B 永远不会被实例化。像这样:

// 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);

我们得到一个新的 A 对象a2,它与 相同a1,意思a2.b是相同的a1.b,意思a2.b.ap是仍然指向a1!这不是我们想要的。我们必须为 A 定义一个复制构造函数,它复制默认复制构造函数的功能,然后将 new 设置A::b.ap为指向新的 A 对象。我们将此代码添加到class 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 );.

但是不需要显式的析构函数;因此,这种情况是三法则的一个例外。我对吗?

4

3 回答 3

9

不要太担心“三法则”。规则不是盲目遵守的;他们在那里让你思考。你想过。你已经得出结论,析构函数不会这样做。所以就不写了。规则的存在是为了不要忘记编写析构函数,从而泄漏资源。

尽管如此,这种设计为 B::ap 创造了错误的可能性。这是一整类潜在的错误,如果它们是一个类,或者以更健壮的方式捆绑在一起,则可以消除它们。

于 2013-03-21T21:33:33.750 回答
4

似乎与B强耦合A,并且总是应该使用A包含它的实例?那A总是包含一个B实例?他们通过友谊访问彼此的私人成员。

因此,人们想知道为什么它们是单独的类。

但是假设您出于其他原因需要两个类,这是一个简单的修复程序,可以消除所有构造函数/析构函数的混淆:

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 一起去。你思考,你决定。实际上有人已经得出结论,通常三法则(如果您在设计期间做出正确的选择)归结为二法则:使您的资源由库对象管理(如上面提到的智能指针),您通常可以摆脱析构函数。如果你足够幸运,你可以摆脱所有这些并依靠编译器为你生成代码。

附带说明:您的复制构造函数不会复制编译器生成的一个。您在其中使用复制赋值,而编译器将使用复制构造函数。摆脱构造函数主体中的分配并使用初始化列表。它会更快更干净。

很好的问题,来自 Ben 的很好的回答(另一个在工作中让我的同事感到困惑的技巧),我很高兴给你们两个投票。

于 2013-03-21T22:41:18.317 回答