【问题标题】:The efficient way to write move copy and move assignment constructors编写移动复制和移动赋值构造函数的有效方法
【发布时间】:2012-09-29 08:09:11
【问题描述】:

以下赋值和复制移动构造函数是最有效的吗? 如果有人有其他方法请告诉我? 我的意思是 std::swap 怎么样?在下面的代码中通过复制构造函数调用赋值是安全的吗?

#include <iostream>
#include <functional>
#include <algorithm>
#include <utility>

using std::cout;
using std::cin;
using std::endl;
using std::bind;


class Widget
{

public:

    Widget(int length)
        :length_(length),
        data_(new int[length])
    {
        cout<<__FUNCTION__<<"("<<length<<")"<<endl;
    }

    ~Widget()
    {
        cout<<endl<<__FUNCTION__<<"()"<<endl;
        if (data_)
        {
            cout<<"deleting source"<<endl;
        } 
        else
        {
            cout<<"deleting Moved object"<<endl;
        }

        cout<<endl<<endl;
    }

    Widget(const Widget& other)
        :length_(other.length_),
        data_(new int[length_])
    {
        cout<<__FUNCTION__<<"(const Widget& other)"<<endl;
        std::copy(other.data_,other.data_ + length_,data_);
    }

    Widget(Widget&& other)
/*
        :length_(other.length_),
        data_(new int[length_])*/
    {
        cout<<__FUNCTION__<<"(Widget&& other)"<<endl;
        length_ = 0;
        data_ = nullptr;
        std::swap(length_,other.length_);
        std::swap(data_,other.data_);
    }

    Widget& operator = (Widget&& other)
    {
        cout<<__FUNCTION__<<"(Widget&& other)"<<endl;

        std::swap(length_,other.length_);
        std::swap(data_,other.data_);

        return *this;
    }

    Widget& operator = (const Widget& other)
    {
        cout<<__FUNCTION__<<"(const Widget& other)"<<endl;
        Widget tem(other);
        std::swap(length_,tem.length_);
        std::swap(data_,tem.data_);

        return *this;
    }
    int length()
    {
        return length_;
    }

private:

    int length_;
    int* data_;
};



int main()
{
    {
        Widget w1(1);
        Widget w2(std::move(Widget(2)));

        w1 = std::move(w2);
    }


    cout<<"ENTER"<<endl;
    cin.get();
    return 0;
}

【问题讨论】:

  • 如果您使用 std::vector 作为类的成员,则不需要编写副本或移动构造函数/赋值(除非您使用的是 VS,您需要移动但不移动副本)。

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


【解决方案1】:

从效率 POV 看起来不错,但包含大量重复代码。我会

  • 为您的类实现 swap() 运算符。
  • 在声明它们的位置初始化 length_data_
  • 尽可能根据其他操作实施操作。

可能想要使用std::memcpy 而不是std::copy,因为无论如何您都在处理原始数组。一些编译器会为您执行此操作,但可能不是全部...

这是您的代码的去重版本。注意只有 一个 地方需要知道Widget 的两个实例是如何交换的。并且只有 一个 地方知道如何分配给定大小的 Widget。

编辑:您通常还希望使用依赖于参数的查找来定位交换,以防万一您有非原始成员。

编辑: 整合了@Philipp 的建议,即让赋值运算符按值接受它的参数。这样,它既充当移动赋值运算符,又充当复制赋值运算符。在移动的情况下,并不是说如果你传递一个临时的,它不会被复制,因为移动构造函数,不是复制构造函数将用于传递参数.

编辑: C++11 允许在右值上调用非成本成员,以与以前版本的标准兼容。这允许像Widget(...) = someWidget 这样的奇怪代码编译。使operator= 需要this 的左值,方法是将&amp; 放在声明防止这种情况之后。请注意,即使没有限制,代码也是正确的,但它似乎是个好主意,所以我添加了它。

编辑:正如 Guillaume Papin 所指出的,析构函数应该使用 delete[] 而不是普通的 delete。 C++ 标准要求通过new [] 分配的内存通过delete [] 删除,即它允许new' andnew []` 使用不同的堆。

class Widget
{
public:
    Widget(int length)
        :length_(length)
        ,data_(new int[length])
    {}

    ~Widget()
    {
        delete[] data_;
    }

    Widget(const Widget& other)
        :Widget(other.length_)
    {
        std::copy(other.data_, other.data_ + length_, data_);
    }

    Widget(Widget&& other)
    {
        swap(*this, other);
    }

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

    int length() const
    {
        return length_;
    }

private:
    friend void swap(Widget& a, Widget& b);

    int length_ = 0;
    int* data_ = nullptr;
};

void swap(Widget& a, Widget& b) {
    using std::swap;
    swap(a.length_, b.length_);
    swap(a.data_, b.data_);
}

【讨论】:

  • 谢谢,我认为这是现在最有效的代码,但是如果类客户端写 someWidget = std::move(anyWidget) = std::move(otherWidget) 我认为将其他指针设为交换后的 nullptrs 是正确答案
  • 只有你事先delete他们,否则你会泄漏内存,不是吗?无论如何,这种双重移动有什么问题? AFAICS,你的例子相当于swap(otherWidget,anyWidget);someWidget=anyWidget,因此相当于someWidget=otherWidget;swap(otherWidget,anyWidget)
  • 被移动对象的指针会指向另一个地方,它们可能在销毁前被使用
  • operator= 的返回值是左值,所以我们必须在移动后使指针为空
  • 析构函数似乎错了,new T[] 不应该有匹配的delete [](而不是单独的delete)吗?
【解决方案2】:

答案是针对@Abdulrhman 在上述 cmets 中的抱怨,即某些(晦涩的)任务序列失败了。放入单独的答案,因为这样更具可读性。

投诉是这样的

Widget w(2);
w = Widget(1) = std::move(w);

崩溃。这是我得到的输出

Widget w(2);
w.data()[0] = 0xDEAD; w.data()[1] = 0xBEEF;
w = Widget(1) = std::move(w);
std::cerr << std::hex << w.data()[0] << w.data()[1] << std::endl;

Widget 中添加了一些代码来记录构造函数、析构函数和赋值运算符的调用。交错是关于这些调用来自哪里的cmets

w is constructed
0x7fff619c36c0: [constructor] allocated 2@0x1043dff80
temporary Widget(1) is constructed
0x7fff619c37c0: [constructor] allocated 1@0x1043e0180
first (right) assignment operator argument is constructed. w is empty afterwards!
0x7fff619c3800: [default constructor] empty
0x7fff619c3800: [move constructor] stealing 2@0x1043dff80 from 0x7fff619c36c0, replacing with 0@0x0
first assignment operator does it's job, i.e. moves from by-value argument.
0x7fff619c37c0: [assignment] stealing 2@0x1043dff80 from 0x7fff619c3800, replacing with 1@0x1043e0180
second (left) assignment operator arguments is constructed
0x7fff619c3780: [constructor] allocated 2@0x1043e0280
0x7fff619c3780: [copy constructor] copying 2@0x1043dff80 from 0x7fff619c37c0
second assignment operator does it's job, i.e. moves from by-value argument
0x7fff619c36c0: [assignment] stealing 2@0x1043e0280 from 0x7fff619c3780, replacing with 0@0x0
second assingment operator's by-value argument is destructed
0x7fff619c3780: [destructor] deleting 0@0x0
first assignment operator's by-value argument is destructed
0x7fff619c3800: [destructor] deleting 1@0x1043e0180
temporary created as Widget(1) is destructed.
0x7fff619c37c0: [destructor] deleting 2@0x1043dff80
data contains in "w" after assignments.
deadbeef
finally, "w" is destructed.
0x7fff619c36c0: [destructor] deleting 2@0x1043e0280

我看不出有什么问题,用 clang 和 -faddress-sanitizer, -fcatch-undefined-behaviour 编译也不会抱怨。

但请注意,第二个分配(左侧 = 运算符)复制而不是移动。这是因为第一个(右)赋值运算符返回一个左值引用。

【讨论】:

  • 好吧,你是对的,这可能是编译器问题,因为没有逻辑错误,虽然 clag 没有发出任何合规的问题,我看到我们必须防止这种无意义的分配序列方式,非常感谢fgp,谢谢大家。
  • @Abdulrhman 你用的是什么编译器?也许你的编译器初始化 length_data_ 错误。您可以尝试在移动构造函数中显式初始化它们,而不是依赖编译器使用声明中的初始化程序。顺便说一句,请考虑接受包含实际代码的答案,而不是这个后续答案。对于看到这个问题的其他人来说,这将不那么令人困惑。
【解决方案3】:

您不需要在移动构造函数中进行如此多的交换和赋值。这个:

Widget(Widget&& other) :
    length( other.length_ ), data( other.data_ )
{
    other.length_ = 0;
    other.data_ = nullptr;
}

为移动构造函数做最少的工作:总共 4 个赋值。您的版本有 8 个,包括对 swap() 的调用。

您的移动分配没问题,但您可能想考虑只写一个 operator=() 来涵盖这两种情况:

Widget &operator=( Widget other ) {
    delete data_;
    data_ = other.data_;
    other.data_ = nullptr;
    length_ = other.length_;
    other.length_ = 0;
    return *this;
}

这比你的版本效率略低,因为它可以移动两次。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-11-18
    • 1970-01-01
    • 2016-09-13
    • 2020-03-22
    • 2019-09-29
    • 2021-05-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多