保持复制构造函数与类属性同步的任何技巧



我相信这是一个常见的问题,但有些谷歌搜索不会返回匹配,因此在这里询问。

所以我有以下课程:

class A {
public:
A(const A &rhs) { m_a = rhs.m_a; }
private:
int m_a;
};

一切都很酷,直到一段时间后,可能是一年后,我向class A添加了一个新属性m_b,但我忘记更新复制构造函数。

它需要一个痛苦的调试来定位不同步。

有没有一个技巧可以避免这样的问题,最好是在构建时?

是的,我可以编写单元测试来覆盖复制构造函数,但当我忘记更新复制构造函数时,很可能我也忘记了单元测试。

最好的方法可能是依赖默认的复制构造函数。例如,您的特定示例(涉及成员副本)与默认构造函数配合良好(即,即使您只是删除了构造函数,行为也会相同)。随着添加更多成员,它们将在默认构造函数中自动接收相同的成员复制行为。

在某些不寻常的情况下,您可能希望强制生成默认构造函数(例如,当您希望为非const源对象具有一些不同的、明确定义的行为时)。在这种情况下,在C++11及更高版本中,您可以显式请求默认的复制构造函数,如下所示:

A(const A&) = default;

一些编码指南还建议始终将上述对默认构造函数的明确请求作为文档形式。

大多数成员都是这样,但有例外

有时,类的大多数成员都可以使用默认的成员副本,但也有几个例外。一个例子是一个原始指针,您希望在其中执行底层数据的深度复制。默认情况下,指针只是被复制,因此源对象和新对象中的指针都将指向内存中的同一位置。

解决方案相当简单:只需将该指针和任何相关联的元数据(例如,如果指向的对象是数组,则为长度字段)包装在一个合适的RAII包装器中,该包装器的复制构造函数执行您想要的特定非默认行为,并在类A中包含此类型的成员。现在A可以继续使用默认的复制构造函数,它为指针调用显式复制构造函数。从本质上讲,您回到了纯成员方式的复制,但使用了指针包装成员的新语义。

这种类型的事情也会帮助你保持你的析构函数,有时你的构造函数也是琐碎的。毫无疑问,上面的原始类有一些delete的原始指针代码。一旦使用复制RAII包装器进行包装,包装器就会负责销毁,因此包含类不必这样做,而且通常可以完全删除析构函数。

在像C++这样的语言中,这种包装器方法的开销通常为零或接近零。

你可以在这个问题中找到更多的细节。

当这不起作用时

有时上述内容可能不起作用。一个例子是复制构造函数需要在某个字段中嵌入新对象的地址(this指针)。自包含包装器无法获取包含对象的this指针(实际上,它甚至不知道自己是成员)。

这里的一种方法是用使用默认复制行为的原始对象的所有字段创建一个新的子对象,然后通过聚合(或继承)这个子对象和少数需要特殊处理的字段来创建新的顶级对象。然后,您可以为所有应该具有默认处理方式的字段保留使用默认副本构造函数。

这种方法甚至可以带来性能的好处,因为编译器除了为异常字段调用显式代码外,还可能对子对象使用纯memcpy方法1。当字段在原始对象中混合在一起时,这种情况发生的可能性或不可能发生的可能性要小得多(例如,因为对象的布局可能会交错异常字段和默认复制字段)。


1这实际上并不意味着代码中会调用标准库memcpy:对于小对象,编译器通常会将其展开为一个展开的序列,其中大部分是最大宽度的加载和存储。

您唯一需要编写复制构造函数、赋值运算符或析构函数的时候是,如果您的类是针对一个资源的RAII包装器。

如果您的类管理多个资源,那么是时候对其进行重构,使其由只管理一个资源的类组成。

示例:

#include <algorithm>
// this class is managing two resources. 
// This will be a maintenance nightmare and will required
// horribly complicated constructor code.
struct DoubleVector
{
int *vec1;
int *vec2;
};

// this class manages a single resource
struct SingleVector
{
SingleVector() : vec (new int[10]) {}
SingleVector(SingleVector const& other) 
: vec (new int[10]) 
{
// note - simple constructor
std::copy(other.vec, other.vec + 10, vec);
}
SingleVector& operator=(SingleVector const& other) 
{
auto temp = other;
std::swap(vec, temp.vec);
return *this;
}
~SingleVector() {
delete [] vec;
}
// note - single resource
int *vec;
};
// this class uses *composition* and default assignment/construction/destruction
// it will never go wrong. And it could not be simpler.
struct DoubleVectorDoneRight
{
SingleVector vec1;
SingleVector vec2;
};
int main()
{
SingleVector v;
SingleVector v2 = v;
SingleVector v3;
v3 = v;
}

打开所有编译器警告。您应该会收到关于任何未初始化的成员变量的警告。在GCC上,其特定标志为-Weffc++。有关更多信息,请参阅此StackOverflow帖子。

此外,尽可能在初始值设定项列表中赋值,而不是在构造函数体中赋值。

相关内容

最新更新