三法则的例外?

Exception to the Rule of Three?

提问人:Sam Kauffman 提问时间:3/22/2013 最后编辑:CommunitySam Kauffman 更新时间:3/22/2013 访问量:1623

问:

我读过很多关于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 对象,它与 相同,表示与 相同,表示仍然指向 !这不是我们想要的。我们必须为 A 定义一个复制构造函数,该构造函数复制默认复制构造函数的功能,然后将 new 设置为指向新的 A 对象。我们将此代码添加到:a2a1a2.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 );

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

C++ 复制构造函数 循环依赖三 法则

评论

9赞 metal 3/22/2013
另请注意,您的包含保护是非法的,因为所有下划线后跟大写字母都是为系统保留的。_B
5赞 metal 3/22/2013
对于 C++11,更好的是零法则:flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html 在这种情况下,您将使用 std::unique_ptr、std::shared_ptr 以及此处的一些用途 std::weak_ptr(或类似的拥有类)来管理 A 和 B 的生存期。这样一来,你的代码读者(包括你)在 6 个月内就揭开了所有的神秘面纱。
1赞 us2012 3/22/2013
@metal 愿意详细说明这有什么帮助吗?我看过那篇文章(诚然很简短),但据我所知,它只涉及资源所有权和生命周期管理,完全忽略了这个问题所涉及的那种“循环”依赖关系。零法则如何处理这种情况?!
1赞 Dave S 3/22/2013
是的,这总体上是一个例外,因为您不需要析构函数(因为 B 实际上并不拥有资源)但是,您需要定义赋值运算符,因为它具有与默认复制构造函数相同的问题。
1赞 us2012 3/22/2013
@metal 也许我在这里很愚蠢,但是 - 当你有循环引用时,它会照顾所有权。它、文章或零法则如何帮助解决这个问题中提到的问题(这不是所有权问题)weak_ptr

答:

4赞 Ben Voigt 3/22/2013 #1

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

因此,人们想知道为什么它们是完全不同的类。

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

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

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

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

您仍然可以使用 containment,并使用宏查找 from 的指针的实例。但这比使用编译器并为您登记指针数学要麻烦得多。ABthisoffsetofstatic_cast

评论

0赞 Sam Kauffman 3/22/2013
嗯,我从没想过以这种方式使用继承。在这个例子中,是继承的唯一目的,对吧?这让我有点不安。也许我无法处理那种优雅。findMyA()
0赞 Ben Voigt 3/22/2013
@Sam:就对象布局而言,这实际上是一个很小的变化:我用一个私有基子对象替换了一个私有成员子对象。
0赞 Sam Kauffman 3/22/2013
啊,这是私人遗产。我以前从未遇到过这种情况。总是有新的东西要学习。
0赞 v.oddou 6/22/2016
那是功能嫉妒。
9赞 dspeyer 3/22/2013 #2

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

尽管如此,这种设计也可能导致 B::ap 出错。这是一整类潜在的错误,如果这些错误是一个单一的类,或者以某种更强大的方式捆绑在一起,就可以消除这些错误。

评论

4赞 Nate C-K 1/30/2015
实际上,我同意问题海报在这里采取的方法。那就是:如果有一个规则,大家似乎都同意,而你正在考虑打破它,不要只是思考,也要征求意见。当你偏离公认的实践时,你就有可能遇到公认的实践所要避免的陷阱,所以不要犹豫,不要放弃许多人多年积累的智慧。
2赞 Tomek 3/22/2013 #3

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

附带说明:您的复制构造函数不会复制编译器生成的构造函数。您可以在其中使用复制赋值,而编译器将使用复制构造函数。删除构造函数正文中的赋值并使用初始值设定项列表。它会更快、更干净。

好问题,本的好答案(另一个在工作中迷惑同事的技巧),我很高兴给你们俩点赞。

评论

0赞 Sam Kauffman 3/22/2013
啊,你对复制构造函数的看法是对的。这是一个疏忽。谢谢。为此投赞成票。