我已经阅读了很多关于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);
我们得到一个与a2
相同的新A对象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 );
的相同过程来实现。
但是没有必要使用明确的析构函数;这种情况是三规则的例外。我是对的吗?
答案 0 :(得分:9)
不要过于担心“三个规则”。规则不是盲目服从的;他们在那里让你思考。你曾经想过。而且你已经得出结论,析构函数不会这样做。所以不要写一个。该规则存在,因此您不会忘记来编写析构函数,从而泄漏资源。
同样,这种设计可能会导致B :: ap出错。这是一整类潜在的错误,如果它们是单个类,或者以更强大的方式捆绑在一起,可以消除。
答案 1 :(得分: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); }
您仍然可以使用包含,并使用A
宏从B
的{{1}}指针中找到this
的实例。但这比使用offsetof
并将编译器登记到指针数学更麻烦。
答案 2 :(得分:2)
我和@dspeyer一起去。你想,你决定。实际上有人已经得出结论,三个规则通常(如果你在设计中做出正确的选择)归结为两个规则:使你的资源由图书馆对象(如上面提到的智能指针)管理,你通常可以摆脱析构函数。如果你足够幸运,你可以摆脱所有,并依靠编译器为你生成代码。
旁注:您的复制构造函数不会复制编译器生成的复制构造函数。您在其中使用复制赋值,而编译器将使用复制构造函数。摆脱构造函数体中的赋值并使用初始化列表。它会更快更清洁。
很好的问题,很好的答案形式Ben(另一个让我的同事在工作中迷惑的伎俩),我很高兴能给你们两个人提供赞成。