三法则的例外?
Posted
技术标签:
【中文标题】三法则的例外?【英文标题】:Exception to the Rule of Three? 【发布时间】:2013-03-11 13:06:18 【问题描述】:我已经阅读了很多关于 C++ Rule of Three 的内容。许多人对此发誓。但是当规定规则时,它几乎总是包含“通常”、“可能”或“可能”这样的词,表示存在例外情况。我还没有看到太多关于这些例外情况可能是什么的讨论——三法则不成立的情况,或者至少坚持三法则没有任何优势的情况。
我的问题是我的情况是否是三法则的合法例外。我相信在我下面描述的情况下,明确定义的复制构造函数和复制赋值运算符是必要的,但是默认(隐式生成)析构函数可以正常工作。这是我的情况:
我有两个类,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 定义一个复制构造函数,它复制默认复制构造函数的功能,然后将新的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
是非法的,因为所有下划线后跟大写字母都是为系统保留的。
对于 C++11,零规则更好:flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html 在这种情况下,您可以使用 std::unique_ptr、std::shared_ptr 和这里有一些用途,std::weak_ptr(或类似的拥有类)。这将为您的代码读者(包括您)在 6 个月内揭开所有谜团。
@metal 想详细说明这有什么帮助?我已经(诚然简短地)看了那篇文章,但据我所知,它只涉及资源所有权和生命周期管理,完全忽略了这个问题所涉及的那种“循环”依赖。零规则如何处理这种情况?!
是的,这总体上是一个例外,因为您不需要析构函数(因为 B 实际上并不拥有资源)但是,您需要定义赋值运算符,因为它有默认复制构造函数也有同样的问题。
@metal 也许我在这里很愚蠢,但是 - weak_ptr
在您有循环引用时会处理 ownership。它、文章或零规则如何帮助解决这个问题中提到的问题(这不是关于所有权的问题)?
【参考方案1】:
不要太担心“三法则”。规则不是盲目遵守的;他们在那里让你思考。你想过。你已经得出结论,析构函数不会这样做。所以就不写了。该规则的存在是为了您不会忘记编写析构函数,从而泄漏资源。
同样,这种设计为 B::ap 创造了错误的可能性。这是一整类潜在的错误,如果它们是一个单独的类,或者以更健壮的方式结合在一起,就可以消除这些错误。
【讨论】:
其实我同意问题发布者在这里采取的方法。那就是:如果有一条似乎每个人都同意的规则,而你正在考虑打破它,不要只是想,还要征求意见。当您偏离公认的做法时,您可能会陷入公认的做法旨在避免的陷阱,因此不要放弃许多人多年积累的智慧。【参考方案2】:似乎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);
您仍然可以使用包含,并使用offsetof
宏从B
的this
指针中找到A
的实例。但这比使用 static_cast
并让编译器为您获取指针数学更麻烦。
【讨论】:
嗯,我从没想过以这种方式使用继承。在这个例子中,findMyA()
是继承的唯一目的,对吧?这让我有点不安。可能我受不了那种优雅。
@Sam:就对象布局而言,这实际上是一个小变化:我用私有基础子对象替换了私有成员子对象。
啊,是私有继承。我以前从未遇到过。总是有新东西要学习。
那是令人羡慕的功能。【参考方案3】:
我选择@dspeyer。你思考,你决定。实际上有人已经得出结论,通常三法则(如果您在设计期间做出正确的选择)归结为二法则:使您的资源由库对象管理(如上面提到的智能指针),您通常可以摆脱析构函数。如果你足够幸运,你可以摆脱所有这些并依赖编译器为你生成代码。
附带说明:您的复制构造函数不会复制编译器生成的构造函数。您在其中使用复制赋值,而编译器将使用复制构造函数。摆脱构造函数主体中的分配并使用初始化列表。它会更快更干净。
Ben 提出了很好的问题,很好的回答(另一个在工作中迷惑同事的技巧),我很高兴给你们两个点赞。
【讨论】:
啊,关于复制构造函数,你是对的。那是一个疏忽。谢谢。对此点赞。以上是关于三法则的例外?的主要内容,如果未能解决你的问题,请参考以下文章