三原则的例外



我读了很多关于c++三法则的书。许多人对它信誓旦旦。但当规则被陈述时,它几乎总是包含一个像"通常"、"可能"或"可能"这样的词,表明有例外。我还没有看到很多关于这些例外情况的讨论——在这些情况下,三法则不成立,或者至少坚持它不会带来任何好处。

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

我有两个类,A和B。这里讨论的是A。B是A的友类。A包含一个B对象。B包含一个A指针,该指针指向拥有B对象的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);

我们得到一个新的a对象a2,它与a1相同,这意味着a2.ba1.b相同,这意味着a2.b.ap仍然指向a1 !这不是我们想要的。我们必须为a定义一个复制构造函数,它复制默认复制构造函数的功能,然后设置新的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 );的相同过程来实现。

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

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

尽管如此,这种设计还是有可能使B::ap出错。这是一整类潜在的bug,如果它们是一个单独的类,或者以某种更健壮的方式捆绑在一起,就可以消除它们。

似乎BA强耦合,并且总是应该使用包含它的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); }

您仍然可以使用包含,并使用offsetof宏从Bthis指针中找到A的实例。但这比使用static_cast和编译器为您调用指针数学更麻烦。

我选@dspeyer。你思考,你决定。实际上,有人已经得出结论,三通常规则(如果你在设计过程中做出了正确的选择)可以归结为二规则:让你的资源由库对象管理(就像上面提到的智能指针),你通常可以摆脱析析函数。如果你足够幸运,你可以摆脱所有,依靠编译器为你生成代码。

边注:你的复制构造函数不会复制编译器生成的一个。你在它里面使用拷贝赋值,而编译器会使用拷贝构造函数。去掉构造函数体中的赋值,使用初始化列表。这样会更快更干净。

Ben问得好,回答得好(这是另一个让我同事困惑的技巧),我很高兴给你们俩点赞。

相关内容

  • 没有找到相关文章

最新更新