我是否违反了三法则?

Am I violating Rule of three?

提问人:Vinayak Garg 提问时间:2/4/2012 最后编辑:Vinayak Garg 更新时间:2/4/2012 访问量:811

问:

我最近读了《三法则》,想知道我是否违反了它?

在我的 GUI 应用程序中,像 、 、 等类(类名是指示性的)每个类都有一个实例。在他们的构造函数中,我分配了一些资源(内存),我在他们的析构函数中安全地释放了这些资源。MainFrameInterfaceCircuitBreadboard

所以我只定义了析构函数,但没有定义复制构造函数赋值运算符

我确定我不需要它们,但我很好奇我是否违反了规则,我可以/应该做些什么来遵守它?

C++ 三法则

评论


答:

6赞 R. Martinho Fernandes 2/4/2012 #1

三法则是关于处理所有三巨头,但这并不一定意味着如果你不想定义它们,就必须定义它们。要么你提供它们,要么你禁止它们。你不应该做的是忽略它们。


所以我只定义了析构函数,但没有定义复制构造函数和复制运算符。
我是否违反了三法则?

是的,你违反了规则。编译器将生成一个复制构造函数和复制赋值运算符,并且由于您在构造函数中分配内存并在析构函数中释放,因此这些副本将具有错误的语义:它们将复制指针,并且您将有两个类别名相同的内存。该赋值甚至不会释放旧内存,而只是覆盖指针。

这是个问题吗?

如果像您暗示的那样,您不制作副本或分配给这些类的实例,则不会出错。但是,最好是安全起见,将复制构造函数和复制赋值运算符声明为私有(甚至不要费心定义),这样就不会意外调用它们。

在 C++11 中,您可以改用以下语法:= delete

T(T const&) = delete; // no copy constructor
T& operator=(T const&) = delete; // no copy assignment

评论

0赞 Vinayak Garg 2/4/2012
第一句话,解决了我的疑惑。 看起来是一个很好的解决方案,只要告诉我这 2 个原型是否也应该私有化(除了= delete= delete)
0赞 R. Martinho Fernandes 2/4/2012
@Vinayak:如果你使用,那么它们是公共的还是私有的都没关系。= delete
0赞 Steve Jessop 2/4/2012
“编译器将生成复制构造函数和复制赋值运算符” - 如果调用方使用它们。当然,在不使用代码时是否生成代码的问题通常没有实际意义,但是如果默认副本是不可能的(例如,如果类具有或引用非静态数据成员),那么这也足以防止事故发生 - 如果有人试图复制,那么编译器将无法生成函数, 否则它甚至不会尝试。就我个人而言,我不会仅仅依靠这一点,我也会删除它们,只是为了让读者清楚地知道它是有意的。const
2赞 adontz 2/4/2012 #2

您应该声明(但不实现)私有副本构造函数和赋值运算符。确保您没有实现这些函数。这将防止对不应复制的类进行任何形式的复制。

评论

2赞 StilesCrisis 2/4/2012
不是空函数。您应该将它们声明为私有,并且永远不要指定任何实现。如果您的代码最终复制/分配对象,这肯定会失败。
0赞 Vinayak Garg 2/4/2012
请解决@StilesCrisis,关注。相应地编辑您的答案
1赞 Sion Sheevok 2/4/2012 #3

是的,根据该定义,它确实违反了三法则。

然而,这是一条“经验法则”。一般准则。如果不需要复制构造或赋值操作,请不要实现它们。其他人则建议将它们声明为私有并将它们定义为空。我会更进一步说,甚至不要定义它们。

如果定义了它们,则仍可能调用空方法。相反,请将它们保留为未定义状态,如果尝试调用这些方法,将收到链接器错误,因为找不到方法定义。优先于构建时错误,而不是运行时错误/意外行为。

2赞 Arunmu 2/4/2012 #4

这很大程度上取决于您的应用程序逻辑以及您如何向用户记录您的接口类。

通常,一个好的 c++ 程序员必须知道 3 法则(如果你知道“复制和交换习惯法”,则为 5 和 1/2,如果是 c++11(移动语义)。

如果你的类管理资源,并且同一个类是可复制的(即复制 ctor 和 assigment 运算符未定义为私有),那么通过编写自己的复制 ctor 和赋值运算符来进行深度复制非常重要。

但是,如果你总是通过将它们作为 REFERENCE 传递来玩弄你的类,那么最好将默认的 copy ctor 和赋值运算符定义为私有的,这样即使你错误地传递了 valy 或复制,编译器也会警告你。

1赞 Paul Manta 2/4/2012 #5

如果你不需要它,就不要遵循它。三法则背后的动机是,当你需要一个析构函数时,这通常是因为你需要做一些动态的释放。

如果还进行解除分配,则还需要复制构造函数和赋值运算符。想象一下,你有一个类,它有一个指向某物的指针:

struct Foo
{
    Foo() { ptr_ = new int; }
    ~Foo() { delete ptr_; }
    int* ptr_;
};

由于您没有定义 copy 构造函数和赋值运算符,因此每当您制作 的副本时,原始副本和副本都将使用指向相同 ;当原始文件或副本被销毁时,指针将被释放,从而使另一个指针具有不可用的数据。Fooint

Foo(cont Foo& other) {
    other.ptr_ = new int(*ptr_);
}

// Same for operator=

如果你没有在构造函数/析构函数中做任何动态分配,那么你实际上很可能不需要复制构造函数或赋值运算符(但不一定)。