提问人:Sam Kauffman 提问时间:3/22/2013 最后编辑:CommunitySam Kauffman 更新时间:3/22/2013 访问量:1623
三法则的例外?
Exception to the Rule of Three?
问:
我读过很多关于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 对象。我们将此代码添加到:a2
a1
a2.b
a1.b
a2.b.ap
a1
A::b.ap
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
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); }
您仍然可以使用 containment,并使用宏查找 from 的指针的实例。但这比使用编译器并为您登记指针数学要麻烦得多。A
B
this
offsetof
static_cast
评论
findMyA()
不要太担心“三法则”。规则不是盲目遵守的;他们在那里让你思考。你已经想过了。你已经得出结论,析构函数不会这样做。所以不要写一个。该规则的存在是为了让您不会忘记编写析构函数,从而泄漏资源。
尽管如此,这种设计也可能导致 B::ap 出错。这是一整类潜在的错误,如果这些错误是一个单一的类,或者以某种更强大的方式捆绑在一起,就可以消除这些错误。
评论
我和@dspeyer一起去。你思考,你决定。实际上,有人已经得出结论,通常三法则(如果你在设计过程中做出了正确的选择)归结为二法则:使你的资源由库对象(如上面提到的智能指针)管理,你通常可以摆脱析构函数。如果你足够幸运,你可以摆脱所有的东西,依靠编译器为你生成代码。
附带说明:您的复制构造函数不会复制编译器生成的构造函数。您可以在其中使用复制赋值,而编译器将使用复制构造函数。删除构造函数正文中的赋值并使用初始值设定项列表。它会更快、更干净。
好问题,本的好答案(另一个在工作中迷惑同事的技巧),我很高兴给你们俩点赞。
评论
_B
weak_ptr