【问题标题】:Best C++ move constructor implementation practice最佳 C++ 移动构造函数实现实践
【发布时间】:2016-04-04 14:26:50
【问题描述】:

我正在尝试了解移动构造函数的实现。 我们都知道,如果我们需要在 C++ 类中管理资源,我们需要实现五法则(C++ 编程)。

微软给我们举了一个例子:https://msdn.microsoft.com/en-us/library/dd293665.aspx

这里更好,它使用复制交换来避免代码重复: Dynamically allocating an array of objects

     // C++11
     A(A&& src) noexcept
         : mSize(0)
         , mArray(NULL)
     {
         // Can we write src.swap(*this);
         // or (*this).swap(src);
         (*this) = std::move(src); // Implements in terms of assignment
     }

在move-constructor中,直接:

         // Can we write src.swap(*this);
         // or (*this).swap(src);

因为我认为(*this) = std::move(src) 有点复杂。因为如果我们不小心写成(*this) = src,它会调用普通的赋值运算符而不是move-assignment-operator。

除了这个问题,在微软的例子中,他们写了这样的代码:在move-assignment-operator中,我们需要检查自赋值吗?有可能发生吗?

// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
   std::cout << "In operator=(MemoryBlock&&). length = " 
             << other._length << "." << std::endl;

   if (this != &other)
   {
      // Free the existing resource.
      delete[] _data;

      // Copy the data pointer and its length from the 
      // source object.
      _data = other._data;
      _length = other._length;

      // Release the data pointer from the source object so that
      // the destructor does not free the memory multiple times.
      other._data = nullptr;
      other._length = 0;
   }
   return *this;
}

【问题讨论】:

  • 移动构造函数对我来说似乎很愚蠢。为什么不只是初始化初始化列表中的所有内容,然后将src 设置为有效的从状态移动?如果您不关心更改已移动对象的大小,则最多应为 4 个分配和 3 个。
  • In the move-constructor, directly: // Can we write src.swap(*this); // or (*this).swap(src); 我不明白为什么不这样做,假设您实现了合适的 swap 方法。
  • 自移动分配有时会发生,特别是在执行swap(*it1, *it2) 的排序算法中,两个迭代器可能会引用同一个元素,这会进行自交换,可能导致一个自我移动。如果您重载了swap(MemoryBlock&amp;, MemoryBlock&amp;),那么这应该不是问题,将调用您的专用交换而不是std::swap。否则不应该发生自移动,尽管理论上它仍然是可能的。
  • @JonathanWakely 有趣的是,我从来没有在赋值运算符中检查过this == &amp;other,因为我认为自赋值只会发生错误,并且不应该掩盖错误,而是尽快发现错误。
  • @MaximEgorushkin:我忘了链接到wg21.link/lwg2468

标签: c++ c++11 move move-constructor


【解决方案1】:

一种方法是实现默认构造函数、复制构造函数和swap函数。

然后使用前三个实现移动构造函数,复制和移动赋值运算符。

例如:

struct X
{
    X();
    X(X const&);
    void swap(X&) noexcept;

    X(X&& b)
        : X() // delegate to the default constructor
    {
        b.swap(*this);
    }

    // Note that this operator implements both copy and move assignments.
    // It accepts its argument by value, which invokes the appropriate (copy or move) constructor.
    X& operator=(X b) {
        b.swap(*this);
        return *this;
    }
};

如果您在 C++98 中一直使用此惯用语,那么添加移动构造函数后,您无需编写任何代码即可获得移动赋值。

In some cases this idiom may be not the most efficient。因为复制操作符总是先构造一个临时的,然后再与之交换。通过手动编码赋值运算符,可以获得更好的性能。如有疑问,请检查优化的程序集输出并使用分析器。

【讨论】:

    【解决方案2】:

    我也在网上寻找实现移动构造函数和移动赋值的最佳方法。有几种方法,但都不是完美的。

    以下是我目前的发现。

    这是一个Test 类,我正在使用该类作为示例:

    class Test {
    private:
      void*       handle_ = nullptr;
      std::string name_;
    
    public:
      Test(std::string name)
        : name_(std::move(name))
      {
      }
        
      ~Test()
      {
        if(handle_) close_handle(handle_);
      } 
    
      Test(Test&& other) noexcept;
      Test& operator=(Test&& other) noexcept;
    
    public:
      friend void swap(Test& v1, Test& v2) noexcept
      {
        std::swap(v1.handle_, v2.handle_);
        std::swap(v1.name_, v2.name_);
      }
    };
    

    方法 #1:直截了当

      Test(Test&& other) noexcept
        : handle_(std::exchange(other.handle_, nullptr))
        , name_(std::move(other.name_))
      {
      }
    
      Test& operator=(Test&& other) noexcept
      {
        if(handle_) close_handle(handle_);
            
        handle_ = std::exchange(other.handle_, nullptr);
        name_ = std::move(other.name_);
    
        return *this;
      }
    

    优点

    • 最佳性能

    缺点

    • 移动构造函数和移动赋值中的代码重复
    • 析构函数的部分代码在移动赋值中重复

    方法 #2:破坏 + 构建

        Test(Test&& other) noexcept
            : handle_(std::exchange(other.handle_, nullptr))
            , name_(std::move(other.name_))
        {
        }
    
        Test& operator=(Test&& other) noexcept
        {
            this->~Test();
            new (this) Test(std::move(other));
    
            return *this;
        }
    

    优点

    • 无代码重复
    • 在没有虚函数的情况下性能良好

    缺点

    • 虚拟方法表 (VMT) 被初始化两次(如果类具有虚拟函数)
    • 不能在基类中使用。基类只能实现移动构造函数。

    方法 #3:复制和交换

        Test(Test&& other) noexcept
            : handle_(std::exchange(other.handle_, nullptr))
            , name_(std::move(other.name_))
        {
        }
    
        Test& operator=(Test&& other) noexcept
        {
            Test (std::move(other)).swap(*this);
            return *this;
        }
    

    或二合一复制和移动运算符:

        Test& operator=(Test other) noexcept
        {
            swap(other, *this);
            return *this;
        }
    

    优点

    • 无代码重复

    缺点

    • 创建了额外的对象
    • swap 函数内交换数据成员时创建的额外更小的对象
    • 交换函数中的某种代码重复

    方法 #4:通过 move-assignment 移动构造函数

    那是你@Dongguo 在 MSDN 上找到的

      Test(Test&& other) noexcept
      {
        *this = std::move(other);
      }
    
      Test& operator=(Test&& other) noexcept
      {
        if(handle_) close_handle(handle_);
            
        handle_ = std::exchange(other.handle_, nullptr);
        name_ = std::move(other.name_);
    
        return *this;
      }
    

    优点

    • 无代码重复

    缺点

    • 不适用于包含非默认可构造数据成员的类。
    • 数据成员在移动构造函数中被初始化两次

    链接

    更多答案

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-04-17
      • 1970-01-01
      • 2021-02-13
      • 2017-09-13
      • 2022-11-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多