什么时候我们必须编写用户定义的复制构造函数?

发布于 2024-09-10 06:43:31 字数 64 浏览 2 评论 0原文

我知道 C++ 编译器为类创建一个复制构造函数。在什么情况下我们必须编写用户定义的复制构造函数?你能举一些例子吗?

I know that C++ compiler creates a copy constructor for a class. In which case do we have to write a user-defined copy constructor? Can you give some examples?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

等风也等你 2024-09-17 06:43:31

编译器生成的复制构造函数执行按成员复制。有时这还不够。例如:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

在这种情况下,按成员方式复制 stored 成员不会复制缓冲区(只会复制指针),因此第一个被销毁的共享缓冲区的副本将调用 delete [] 成功,第二个将遇到未定义的行为。您需要深度复制复制构造函数(以及赋值运算符)。

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}

The copy constructor generated by the compiler does member-wise copying. Sometimes that is not sufficient. For example:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

in this case member-wise copying of stored member will not duplicate the buffer (only the pointer will be copied), so the first to be destroyed copy sharing the buffer will call delete[] successfully and the second will run into undefined behavior. You need deep copying copy constructor (and assignment operator as well).

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}
找回味觉 2024-09-17 06:43:31

我对没有引用五规则的规则感到有点恼火。

这个规则非常简单:

五法则
每当您编写析构函数、复制构造函数、复制赋值运算符、移动构造函数或移动赋值运算符之一时,您可能需要编写其他四个。

但是您应该遵循一个更通用的准则,该准则源于编写异常安全代码的需要:

每个资源应该由专用对象管理

这里@sharptooth的代码仍然(大部分)很好,但是如果他要向他的类添加第二个属性,那就不行了。考虑以下类:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

如果 new Bar 抛出异常,会发生什么?如何删除 mFoo 指向的对象?有一些解决方案(功能级别的 try/catch ...),但它们只是无法扩展。

处理这种情况的正确方法是使用正确的类而不是原始指针。

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

使用相同的构造函数实现(或者实际上使用 make_unique),我现在可以免费获得异常安全!!!是不是很刺激呢?最重要的是,我不再需要担心合适的析构函数!我确实需要编写自己的复制构造函数赋值运算符,因为unique_ptr没有定义这些操作......但它没有这里很重要;)

因此,重新审视sharptooth的课程:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

我不了解你,但我发现我的课程更容易;)

I am a bit peeved that the rule of the Rule of Five wasn't cited.

This rule is very simple:

The Rule of Five:
Whenever you are writing either one of Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor or Move Assignment Operator you probably need to write the other four.

But there is a more general guideline that you should follow, which derives from the need to write exception-safe code:

Each resource should be managed by a dedicated object

Here @sharptooth's code is still (mostly) fine, however if he were to add a second attribute to his class it would not be. Consider the following class:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

What happens if new Bar throws ? How do you delete the object pointed to by mFoo ? There are solutions (function level try/catch ...), they just don't scale.

The proper way to deal with the situation is to use proper classes instead of raw pointers.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

With the same constructor implementation (or actually, using make_unique), I now have exception safety for free!!! Isn't it exciting ? And best of all, I no longer need to worry about a proper destructor! I do need to write my own Copy Constructor and Assignment Operator though, because unique_ptr does not define these operations... but it doesn't matter here ;)

And therefore, sharptooth's class revisited:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

I don't know about you, but I find mine easier ;)

榆西 2024-09-17 06:43:31

当人们必须显式声明/定义复制构造函数时,我可以回忆起我的实践并想到以下情况。我将这些情况分为两类

  • 正确性/语义 - 如果您不提供用户定义的复制构造函数,使用该类型的程序可能无法编译,或者可能无法正常工作。
  • 优化 - 为编译器生成的复制构造函数提供一个很好的替代方案,可以使程序更快。


正确性/语义

我在本节中放置了声明/定义复制构造函数对于使用该类型的程序的正确操作是必要的情况。

读完本节后,您将了解允许编译器自行生成复制构造函数的几个陷阱。因此,正如 seand 在他的 回答,关闭新类的可复制性并在稍后真正需要时故意启用它总是安全的。

如何在 C++03 中使类不可复制

声明一个私有复制构造函数并且不提供它的实现(这样即使在类中复制了该类型的对象,构建也会在链接阶段失败)自己的范围或由其朋友)。

如何在 C++11 或更新版本中使类不可复制

在最后声明复制构造函数 =delete


浅复制与深复制

这是最好理解的情况,实际上也是其他答案中唯一提到的情况。 shaprtooth覆盖 非常好。我只想补充一点,深度复制本应由对象独占的资源可以适用于任何类型的资源,动态分配的内存只是其中一种。如果需要,深度复制对象可能还需要

  • 复制磁盘上的临时文件
  • ,打开单独的网络连接,
  • 创建单独的工作线程
  • ,分配单独的 OpenGL 帧缓冲区

自注册对象

考虑一个类,其中所有对象 -无论它们是如何构建的 - 都必须以某种方式进行注册。一些示例:

  • 最简单的示例:维护当前存在对象的总数。对象注册只是增加静态计数器。

  • 一个更复杂的示例是拥有一个单例注册表,其中存储对该类型的所有现有对象的引用(以便可以将通知传递给所有这些对象)。

  • 引用计数智能指针可以被视为此类别的特殊情况:新指针将自身“注册”到共享资源而不是全局注册表中。

这种自注册操作必须由该类型的任何构造函数执行,复制构造函数也不例外。


具有内部交叉引用的对象

某些对象可能具有不平凡的内部结构,其不同子对象之间具有直接交叉引用(事实上,仅一个这样的内部交叉引用就足以触发这种情况)。编译器提供的复制构造函数将破坏内部对象内关联,将其转换为对象间关联。

示例:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

仅允许复制满足特定条件的对象

可能存在这样的类,其中对象在某种状态(例如默认构造状态)下可以安全复制,而在其他情况下则安全复制。如果我们想要允许复制安全复制对象,那么 - 如果进行防御性编程 - 我们需要在用户定义的复制构造函数中进行运行时检查。


不可复制的子对象

有时,一个应该可复制的类会聚合不可复制的子对象。
通常,这种情况发生在具有不可观察状态的对象上(这种情况将在下面的“优化”部分中更详细地讨论)。编译器只是帮助识别这种情况。


准可复制子对象

应该是可复制的类可以聚合准可复制类型的子对象。准可复制类型不提供严格意义上的复制构造函数,但具有另一个允许创建对象的概念副本的构造函数。使类型准复制的原因是当类型的复制语义没有完全一致时。

例如,重新审视对象自注册案例,我们可以认为
可能存在必须向全局注册对象的情况
仅当对象管理器是完整的独立对象时。如果它是一个
另一个对象的子对象,那么管理它的责任是
它包含的对象。

或者,必须支持浅复制和深复制(它们都不是默认值)。

然后,最终决定权留给该类型的用户 - 复制对象时,他们必须明确指定(通过附加参数)预期的复制方法。

在非防御性编程方法的情况下,也可能同时存在常规复制构造函数和准复制构造函数。当在绝大多数情况下应应用单一复制方法时,这是合理的,而在极少数但易于理解的情况下,应使用替代复制方法。那么编译器就不会抱怨它无法隐式定义复制构造函数;用户有责任记住并检查是否应该通过准复制构造函数复制该类型的子对象。


不要复制与对象身份强关联的状态

在极少数情况下,对象的可观察状态的子集可能构成(或被视为)对象的不可分割的一部分身份并且不应该转移到其他对象(尽管这可能有些争议)。

示例:

  • 对象的 UID(但这也属于上面的“自注册”情况,因为 id 必须通过自注册行为获得)。

    对象

  • 在新对象不得继承源对象的历史记录,而是以单个历史记录项“复制于

在这种情况下,复制构造函数必须跳过复制相应的子对象。


强制复制构造函数的正确签名

编译器提供的复制构造函数的签名取决于子对象可用的复制构造函数。如果至少一个子对象没有真正的复制构造函数(通过常量引用获取源对象),而是具有变异复制构造函数(通过常量引用获取源对象)通过非常量引用创建对象),那么编译器将别无选择,只能隐式声明然后定义一个可变的复制构造函数。

现在,如果子对象类型的“变异”复制构造函数实际上并未改变源对象(而只是由不了解 const 关键字的程序员编写的),该怎么办? ?如果我们无法通过添加缺少的 const 来修复该代码,那么另一个选择是使用正确的签名声明我们自己的用户定义的复制构造函数,并犯下转向 的错误。 >const_cast


写时复制 (COW)

已放弃对其内部数据的直接引用的 COW 容器必须在构造时进行深度复制,否则它可能会充当引用计数句柄。

虽然COW是一种优化技术,但这个逻辑在复制构造函数中
对其正确实施至关重要。这就是为什么我把这个案例放在这里
而不是我们接下来要讨论的“优化”部分。



优化

在以下情况下,出于优化考虑,您可能希望/需要定义自己的复制构造函数:


复制过程中的结构优化

考虑一个支持元素删除操作的容器,但可以通过简单地标记已删除的元素来实现元素被删除,并稍后回收其插槽。当制作这样的容器的副本时,压缩幸存的数据而不是按原样保留“已删除”的槽可能是有意义的。


跳过复制不可观察状态

对象可能包含不属于其可观察状态的数据。通常,这是在对象的生命周期内积累的缓存/记忆数据,以便加速对象执行的某些缓慢查询操作。跳过复制该数据是安全的,因为当(并且如果!)执行相关操作时将重新计算数据。复制这些数据可能是不合理的,因为如果通过变异操作修改对象的可观察状态(从中派生缓存数据),它可能会很快失效(如果我们不打算修改对象,为什么我们要创建一个深度对象)然后复制?)

仅当辅助数据与表示可观察状态的数据相比较大时,这种优化才是合理的。


禁用隐式复制

C++ 允许通过声明复制构造函数显式 来禁用隐式复制。那么该类的对象就不能传递到函数中和/或按值从函数返回。此技巧可用于看似轻量级但复制成本确实非常昂贵的类型(尽管使其准可复制可能是更好的选择)。

在 C++03 中声明复制构造函数也需要定义它(当然,如果
你打算使用它)。因此,寻找这样的复制构造函数只是为了
所讨论的问题意味着您必须编写与
编译器会自动为您生成。

C++11 和更新的标准允许声明特殊成员函数(
默认构造函数和复制构造函数、复制赋值运算符以及
析构函数),明确请求使用默认实现
(只需以 =default 结束声明即可)。



待办事项

这个答案可以改进如下:

  • 添加更多示例代码
  • 说明“具有内部交叉引用的对象”案例
  • 添加一些链接

I can recall from my practice and think of the following cases when one has to deal with explicitly declaring/defining the copy constructor. I have grouped the cases into two categories

  • Correctness/Semantics - if you don't provide a user-defined copy-constructor, programs using that type may fail to compile, or may work incorrectly.
  • Optimization - providing a good alternative to the compiler-generated copy constructor allows to make the program faster.


Correctness/Semantics

I place in this section the cases where declaring/defining the copy constructor is necessary for the correct operation of the programs using that type.

After reading through this section, you will learn about several pitfalls of allowing the compiler to generate the copy constructor on its own. Therefore, as seand noted in his answer, it is always safe to turn off copyability for a new class and deliberately enable it later when really needed.

How to make a class non-copyable in C++03

Declare a private copy-constructor and don't provide an implementation for it (so that the build fails at linking stage even if the objects of that type are copied in the class' own scope or by its friends).

How to make a class non-copyable in C++11 or newer

Declare the copy-constructor with =delete at end.


Shallow vs Deep Copy

This is the best understood case and actually the only one mentioned in the other answers. shaprtooth has covered it pretty well. I only want to add that deeply copying resources that should be exclusively owned by the object can apply to any type of resources, of which dynamically allocated memory is just one kind. If needed, deeply copying an object may also require

  • copying temporary files on the disk
  • opening a separate network connection
  • creating a separate worker thread
  • allocating a separate OpenGL framebuffer
  • etc

Self-registering objects

Consider a class where all objects - no matter how they have been constructed - MUST be somehow registered. Some examples:

  • The simplest example: maintaining the total count of currently existing objects. Object registration is just about incrementing the static counter.

  • A more complex example is having a singleton registry, where references to all existing objects of that type are stored (so that notifications can be delivered to all of them).

  • Reference counted smart-pointers can be considered just a special case in this category: the new pointer "registers" itself with the shared resource rather than in a global registry.

Such a self-registration operation must be performed by ANY constructor of the type and the copy constructor is no exception.


Objects with internal cross-references

Some objects may have non-trivial internal structure with direct cross-references between their different sub-objects (in fact, just one such internal cross-reference is enough to trigger this case). The compiler-provided copy constructor will break the internal intra-object associations, converting them to inter-object associations.

An example:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Only objects meeting certain criteria are allowed to be copied

There may be classes where objects are safe to copy while in some state (e.g. default-constructed-state) and not safe to copy otherwise. If we want to allow copying safe-to-copy objects, then - if programming defensively - we need a run-time check in the user-defined copy constructor.


Non-copyable sub-objects

Sometimes, a class that should be copyable aggregates non-copyable sub-objects.
Usually, this happens for objects with non-observable state (that case is discussed in more detail in the "Optimization" section below). The compiler merely helps to recognize that case.


Quasi-copyable sub-objects

A class, that should be copyable, may aggregate a sub-object of a quasi-copyable type. A quasi-copyable type doesn't provide a copy constructor in the strict sense, but has another constructor that allows to create a conceptual copy of the object. The reason for making a type quasi-copyable is when there is no full agreement about the copy semantics of the type.

For example, revisiting the object self-registration case, we can argue that
there may be situations where an object must be registered with the global
object manager only if it is a complete standalone object. If it is a
sub-object of another object, then the responsibility of managing it is with
its containing object.

Or, both shallow and deep copying must be supported (none of them being the default).

Then the final decision is left to the users of that type - when copying objects, they must explicitly specify (through additional arguments) the intended method of copying.

In case of a non-defensive approach to programming, it is also possible that both a regular copy-constructor and a quasi-copy-constructor are present. This can be justified when in the vast majority of cases a single copying method should be applied, while in rare but well understood situations alternative copying methods should be used. Then the compiler won't complain that it is unable to implicitly define the copy constructor; it will be the users' sole responsibility to remember and check whether a sub-object of that type should be copied via a quasi-copy-constructor.


Don't copy state that is strongly associated with the object's identity

In rare cases a subset of the object's observable state may constitute (or be considered) an inseparable part of the object's identity and should not be transferable to other objects (though this can be somewhat controversial).

Examples:

  • The UID of the object (but this one also belongs to the "self-registration" case from above, since the id must be obtained in an act of self-registration).

  • History of the object (e.g. the Undo/Redo stack) in the case when the new object must not inherit the history of the source object, but instead start with a single history item "Copied at <TIME> from <OTHER_OBJECT_ID>".

In such cases the copy constructor must skip copying the corresponding sub-objects.


Enforcing correct signature of the copy constructor

The signature of the compiler-provided copy constructor depends on what copy constructors are available for the sub-objects. If at least one sub-object doesn't have a real copy constructor (taking the source object by constant reference) but instead has a mutating copy-constructor (taking the source object by non-constant reference) then the compiler will have no choice but to implicitly declare and then define a mutating copy-constructor.

Now, what if the "mutating" copy-constructor of the sub-object's type doesn't actually mutate the source object (and was simply written by a programmer who doesn't know about the const keyword)? If we can't have that code fixed by adding the missing const, then the other option is to declare our own user-defined copy constructor with a correct signature and commit the sin of turning to a const_cast.


Copy-on-write (COW)

A COW container that has given away direct references to its internal data MUST be deep-copied at the time of construction, otherwise it may behave as a reference counting handle.

Though COW is an optimization technique, this logic in the copy constructor
is crucial for its correct implementation. That is why I placed this case here
rather than in the "Optimization" section, where we go next.



Optimization

In the following cases you may want/need to define your own copy constructor out of optimization concerns:


Structure optimization during copy

Consider a container that supports element removal operations, but may do so by simply marking the removed element as deleted, and recycle its slot later. When a copy of such a container is made, it may make sense to compact the surviving data rather than preserve the "deleted" slots as is.


Skip copying non-observable state

An object may contain data that is not part of its observable state. Usually, this is cached/memoized data accumulated over the object's lifetime in order to speed-up certain slow query operations performed by the object. It is safe to skip copying that data since it will be recalculated when (and if!) the relevant operations are performed. Copying this data may be unjustified, as it may be quickly invalidated if the object's observable state (from which the cached data is derived) is modified by mutating operations (and if we are not going to modify the object, why are we creating a deep copy then?)

This optimization is justified only if the auxiliary data is large compared to the data representing the observable state.


Disable implicit copying

C++ allows to disable implicit copying by declaring the copy constructor explicit. Then objects of that class cannot be passed into functions and/or returned from functions by value. This trick can be used for a type that appears to be lightweight but is indeed very expensive to copy (though, making it quasi-copyable might be a better choice).

In C++03 declaring a copy constructor required defining it too (of course, if
you intended to use it). Hence, going for such a copy constructor merely out
of the concern being discussed meant that you had to write the same code that
the compiler would automatically generate for you.

C++11 and newer standards allow declaring special member functions (the
default and copy constructors, the copy-assignment operator, and the
destructor) with an explicit request to use the default implementation
(just end the declaration with =default).



TODOs

This answer can be improved as follows:

  • Add more example code
  • Illustrate the "Objects with internal cross-references" case
  • Add some links
我最亲爱的 2024-09-17 06:43:31

如果您有一个具有动态分配内容的类。例如,您将一本书的标题存储为 char * 并将标题设置为 new,复制将不起作用。

您必须编写一个复制构造函数,先执行title = new char[length+1],然后执行strcpy(title, titleIn)。复制构造函数只会执行“浅”复制。

If you have a class that has dynamically allocated content. For example you store the title of a book as a char * and set the title with new, copy will not work.

You would have to write a copy constructor that does title = new char[length+1] and then strcpy(title, titleIn). The copy constructor would just do a "shallow" copy.

生生漫 2024-09-17 06:43:31

当对象按值传递、按值返回或显式复制时,将调用复制构造函数。如果没有复制构造函数,C++ 将创建一个默认的复制构造函数来进行浅复制。如果对象没有指向动态分配内存的指针,则可以使用浅复制。

Copy Constructor is called when an object is either passed by value, returned by value, or explicitly copied. If there is no copy constructor, c++ creates a default copy constructor which makes a shallow copy. If the object has no pointers to dynamically allocated memory then shallow copy will do.

酒废 2024-09-17 06:43:31

除非类特别需要它,否则禁用复制构造函数和运算符=通常是个好主意。这可以防止效率低下,例如在需要引用时按值传递 arg。编译器生成的方法也可能无效。

It's often a good idea to disable copy ctor, and operator= unless the class specifically needs it. This may prevent inefficiencies such as passing an arg by value when reference is intended. Also the compiler generated methods may be invalid.

听不够的曲调 2024-09-17 06:43:31

让我们考虑下面的代码片段:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData(); 给出垃圾输出,因为创建了一个用户定义的复制构造函数,而没有编写显式复制数据的代码。所以编译器不会创建相同的。

只是想与大家分享这些知识,尽管你们大多数人已经知道了。

干杯...
快乐编码!

Let's consider below code snippet:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData(); gives junk output because there is a user-defined copy-constructor created with no code written to copy data explicitly. So compiler does not create the same.

Just thought of sharing this knowledge with you all, although most of you know it already.

Cheers...
Happy coding!!!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文