【问题标题】:C++ Rule of 5 copy and move (constructor and assignment) caveat: to copy or moveC++ 5 复制和移动规则(构造函数和赋值)警告:复制或移动
【发布时间】:2015-04-06 04:48:52
【问题描述】:

我正在编写一个符合 c++11+ 标准的类,是时候实现 5 规则了。

  1. Destructor
  2. Copy Constructor
  3. Move Constructor
  4. Copy Assignment Operator
  5. Move Assignment Operator

我有一个关于复制/移动构造函数/赋值的问题。我的理解是复制构造函数/赋值应该复制你的类(浅的,深的?)。如果您的类具有唯一成员(例如 unique_ptr),我可以预见两种情况。

  • 制作对象的深层副本

    就我而言,我不确定如何制作深层副本(请参阅下面的代码)。

  • 将对象移动到其他类

    在我看来,在复制构造函数中移动指针会给用户带来意想不到的副作用,因为他们期望的是复制而不是移动,并且被复制的原始对象将不再起作用。

    复制也可能有问题,但是,就我而言,curl 对象可能包含敏感信息,例如 cookie 或密码?


为具有这些约束的类创建复制和移动构造函数/赋值的实际方法是什么?要深拷贝、移动还是不显式和隐式定义复制构造函数 (does the delete keyword do this)?


// client.h
#pragma once

#include <Poco/URI.h>
#include <curl/curl.h>

class client
{
public:
    typedef Poco::URI uri_type;
    // Constructor
    client(const uri_type & auth);
    // Destructor
    virtual ~client();
    // Copy constructor
    client(const client & other);
    // Move constructor
    client(client && other);
    // Copy assignment
    client & operator=(const client & other);
    // Move assignment operator
    client & operator=(client && other);

private:
    uri_type auth_;
    // ... other variables (both unique and copyable) ommitted for simplicity.
    std::unique_ptr<CURL, void(*)(CURL*)> ptr_curl_;
};

// client.cpp
#include <memory>
#include <Poco/URI.h>
#include <curl/curl.h>

#include "client.h"

// Constructor
client::client(const uri_type & auth)
: auth_(auth)
, ptr_curl_(curl_easy_init(), curl_easy_cleanup)
{
    curl_global_init(CURL_GLOBAL_DEFAULT);
}

// Destructor
client::~client()
{
    curl_global_cleanup();
}

// Copy constructor
client::client(const client & other)
{
    // ... deep copy? move?
    // how would you deep copy a unique_ptr<CURL>?
}

// Move constructor
client::client(client && other)
{
    std::swap(*this, other);
}

// Copy assignment
client & client::operator=(const client & other)
{
    // Cant get this to work by making the copy happen in the parameter.
    client temp(other);
    std::swap(*this, temp);
    return *this;
}

// Move assignment operator
client & client::operator=(client && other)
{
    return *this;
}

【问题讨论】:

  • 作为一个指导原则,当不确定是否支持移动或复制这样一些更复杂的情况时,首先让你的类不可复制,然后让客户端代码的需求成为更清晰 - 驱动您是否添加对复制和/或移动的支持,以及使用什么确切的语义,即浅与深,重新建立连接/cookie 等。如果您无法清楚地看到客户端需要工作什么,那就太早了完成设计。
  • @vsoftco 我不同意。遵循 0 规则的人让我必须在我想调试的任何时候为他们实现 5 种方法……如果你认为你不会通过调试来找出特定类的构建时间,那就再给你的职业生涯一些年。
  • @ChristopherPisz 在这种情况下,我只需实现一个跟踪对象的基类(即计数器、构造/销毁),然后在调试模式下从它派生(如class MyClass #ifdef DEBUG : public Counter&lt;MyClass&gt; #endif),请参阅例如en.wikipedia.org/wiki/…。我仍然认为代码应该尽可能简单,并且应该尽可能多地重复使用。
  • @vsoftco 这似乎与保持它尽可能简单的想法背道而驰。您实际上是在编译器生成的代码之上引入更多代码来完成调试任务。请参阅此处的相关讨论:softwareengineering.stackexchange.com/questions/361392/…这周我一直在讨论与零相关的几个主题:)

标签: c++ move copy-constructor


【解决方案1】:

顾名思义,复制构造函数/赋值运算符应始终复制而不移动其成员,其中复制通常表示深度复制。

记住:默认情况下,c++ 中的所有对象都应该具有值语义,即它们的行为应该像int

此外,您帖子中的术语表明您将唯一对象(单例)与unique_ptr 指向的对象混淆了。大多数不可复制的对象都是处理程序(如 unique_ptr 处理堆上的对象),在这种情况下,您将复制它们处理的任何内容。如果这是不可能的,那么很可能,实现对象的复制构造函数根本没有意义。

如果您的对象拥有对唯一资源的拥有引用(在您的项目中只能有一个实例),那么第一个问题是:可以共享吗? -> 使用 shared_ptr。如果不是 -> 不要复制。 如果您的对象持有对唯一资源(原始指针或引用)的非拥有引用,请复制该引用。在这两种情况下,请注意您现在有两个对象,它们共享部分状态,即使在非多线程应用程序中也可能很危险。

【讨论】:

  • Most other posts 在堆栈溢出中说如果复制构造函数是唯一的,则在复制构造函数中移动,这个问题需要澄清。
  • @FranciscoAguilera:呃,这到底是说在哪里实现了一个破坏性的复制构造函数?这是一篇很长的帖子,因为您已经确定了一些您关心的问题,所以您分享它比让我再次阅读整个页面更有意义。
  • @FranciscoAguilera:那是关于如何实现复制,而不是是否移动或复制
  • @BenVoigt 那么交换本质上不就是这样吗?当我想到在现实生活中交换一些东西时,你会失去一个对象并得到一个不同的对象,就像移动一样?
  • 另外,我的问题仍然归结为是创建深层副本(可能存在安全问题)还是通过指定删除来禁用复制。
【解决方案2】:

对于某些类型,使用复制构造函数是不合适的。这包括语义上包含不可复制类型的大多数类型。 (不要算unique_ptr,算CURL实例)

如果 libcurl 具有“duplicate_handle”类型的函数,那么您的复制构造函数应该使用它来初始化副本中的 unique_ptr。否则,您应该删除您的复制构造函数并仅实现移动。

【讨论】:

  • 好吧,如果“新句柄不会继承任何状态信息、没有连接、没有 SSL 会话和没有 cookie”是您在复制 client 实例时所期望的,那么这就是去吧。
  • 它声明“输入句柄已被告知指向(而不是复制)之前使用 char * 输入调用 curl_easy_setopt 的所有字符串,也将由新句柄指向。因此,您必须确保保留数据,直到两个句柄都被清理干净。”因此,无论如何这都不是正确的副本...
  • 回到第一方,在我的具体情况下,我仍然不确定是否使用 duphandle 进行复制或不允许使用 delete 进行复制。
  • @FranciscoAguilera:您必须问自己的问题是:复制客户端对象在语义上是否有意义(在您的程序中)以及您对该副本的期望。如果复制它没有意义,则删除复制构造函数和赋值运算符。如果有副本有意义,但语义不明显,则删除副本函数并编写一个命名的副本函数,如 (createClientToSameServer())。
  • @Francisco Aguilera:我也会这么说。
猜你喜欢
  • 1970-01-01
  • 2016-09-13
  • 2019-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-31
  • 2023-03-28
相关资源
最近更新 更多