C++统一赋值运算符移动语义

20

编辑:已解决,请参见评论。 - 不知道如何在没有答案的情况下标记为已解决。

在观看了 Channel 9 上有关 c++0x 中完美转发 / 移动语义的视频后,我有些倾向于认为这是编写新赋值运算符的好方法。

#include <string>
#include <vector>
#include <iostream>

struct my_type 
{
    my_type(std::string name_)
            :    name(name_)
            {}

    my_type(const my_type&)=default;

    my_type(my_type&& other)
    {
            this->swap(other);
    }

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

    void swap(my_type &other)
    {
            name.swap(other.name);
    }

private:
    std::string name;
    void operator=(const my_type&)=delete;  
    void operator=(my_type&&)=delete;
};


int main()
{
    my_type t("hello world");
    my_type t1("foo bar");
    t=t1;
    t=std::move(t1);
}

这应该允许将r-values和const&赋值给它。通过使用适当的构造函数构造一个新对象,然后交换其内容与*this。在我的看来,这似乎是可行的,因为没有多余的数据被复制。而且指针算术是便宜的。

然而,我的编译器不同意。(g++ 4.6) 我得到了以下错误。

copyconsttest.cpp: In function ‘int main()’:
copyconsttest.cpp:40:4: error: ambiguous overload for ‘operator=’ in ‘t = t1’
copyconsttest.cpp:40:4: note: candidates are:
copyconsttest.cpp:18:11: note: my_type& my_type::operator=(my_type)
copyconsttest.cpp:30:11: note: my_type& my_type::operator=(const my_type&) <deleted>
copyconsttest.cpp:31:11: note: my_type& my_type::operator=(my_type&&) <near match>
copyconsttest.cpp:31:11: note:   no known conversion for argument 1 from ‘my_type’ to ‘my_type&&’
copyconsttest.cpp:41:16: error: ambiguous overload for ‘operator=’ in ‘t = std::move [with _Tp = my_type&, typename std::remove_reference< <template-parameter-1-1> >::type = my_type]((* & t1))’
copyconsttest.cpp:41:16: note: candidates are:
copyconsttest.cpp:18:11: note: my_type& my_type::operator=(my_type)
copyconsttest.cpp:30:11: note: my_type& my_type::operator=(const my_type&) <deleted>
copyconsttest.cpp:31:11: note: my_type& my_type::operator=(my_type&&) <deleted>

我做错了什么吗?这是不好的实践吗(我认为没有测试自我分配的方法)?还是编译器还没有准备好?

谢谢


1
你为什么删除了这两个赋值运算符?如果我没有漏掉什么,那么简单地忽略它们就足够了。 - Dennis Zickefoose
еҰӮжһңдҪ жғідҪҝз”Ёд»»дҪ•"+swap"д№ иҜӯпјҢжҲ‘зӣёдҝЎдҪ еҝ…йЎ»дёәе·ҰеҖје’ҢеҸіеҖјеј•з”Ёе®һзҺ°swapеҮҪж•°гҖӮ然еҗҺе®ғеә”иҜҘжҳҜswap(std::move(other));гҖӮ - Kerrek SB
@Kerrek:不,因为在类内部,你总是最终得到l-values。 - Dennis Zickefoose
@Dennis Zickefoose 对不起,这是我对删除运算符的理解错误。我以为删除它会确保我不会得到一个隐式编译器生成的运算符,但实际上它并不能阻止其他匹配重载函数的工作。再次感谢。 - 111111
1
@111111:如果要标记为已解决但没有答案:要么自己提供正确答案并接受,要么如果问题不再相关,则删除该问题。 - sehe
4
顺便说一句,我认为如果只是因为Howard Hinnant的出色帖子,这个问题可以保留下来。 如果让它消失了,那就太可惜了。 - sehe
3个回答

25

谨慎使用copy/swap赋值习惯用法,它可能不够优化,特别是在没有仔细分析的情况下应用。即使您需要为赋值运算符提供强异常安全性,也可以通过其他方式实现该功能。

对于您的示例,我建议:

struct my_type 
{
    my_type(std::string name_)
            :    name(std::move(name_))
            {}

    void swap(my_type &other)
    {
            name.swap(other.name);
    }

private:
    std::string name;
};
这将为您提供隐式的复制和移动语义,以前往std :: string的复制和移动成员。而std :: string的作者最了解如何完成这些操作。
如果您的编译器尚不支持隐式移动生成,但支持默认的特殊成员,那么可以使用以下方法:
struct my_type 
{
    my_type(std::string name_)
            :    name(std::move(name_))
            {}

    my_type(const mytype&) = default;
    my_type& operator=(const mytype&) = default;
    my_type(mytype&&) = default;
    my_type& operator=(mytype&&) = default;

    void swap(my_type &other)
    {
            name.swap(other.name);
    }

private:
    std::string name;
};

如果您只是想明确指定特殊成员函数,您也可以选择执行上述操作。

如果您使用的编译器尚未支持默认的特殊成员函数(或隐式移动成员函数),则可以显式地提供编译器在完全符合C ++ 11标准时应默认生成的内容:

struct my_type 
{
    my_type(std::string name_)
            :    name(std::move(name_))
            {}

    my_type(const mytype& other)
        : name(other.name) {}
    my_type& operator=(const mytype& other)
    {
        name = other.name;
        return *this;
    }
    my_type(mytype&& other)
        : name(std::move(other.name)) {}
    my_type& operator=(mytype&& other)
    {
        name = std::move(other.name);
        return *this;
    }

    void swap(my_type &other)
    {
            name.swap(other.name);
    }

private:
    std::string name;
};
如果你真的需要对赋值操作具有强异常安全性,就要进行一次设计,并对其进行明确说明(编辑以包括 Luc Danton 的建议):
template <class C>
typename std::enable_if
<
    std::is_nothrow_move_assignable<C>::value,
    C&
>::type
strong_assign(C& c, C other)
{
    c = std::move(other);
    return c;
}

template <class C>
typename std::enable_if
<
    !std::is_nothrow_move_assignable<C>::value,
    C&
>::type
strong_assign(C& c, C other)
{
    using std::swap;
    static_assert(std::is_nothrow_swappable_v<C>,  // C++17 only
                  "Not safe if you move other into this function");
    swap(c, other);
    return c;
}

现在,您的客户可以选择效率(使用my type::operator=)或使用strong_assign来获得强异常安全性。


感谢您的热情回应。我的问题是,我如何知道编译器是否真正选择正确地执行这些操作。通过使用移动语义,我开始返回越来越重的对象到“堆栈”上,并且我知道移动语义使得这个过程变得廉价。当然,如果编译器按照我所希望的那样执行,那么我就会不断地复制大量的字符串和其他重型容器,这将给自己带来麻烦。尽管如此,非常感谢您的答案,我会按照您的建议去做。 - 111111
1
如果对象很“重”,那么我想知道移动语义是否很重要。移动语义对于管理重型资源的轻量级控制器对象非常有用,其中深度复制资源是不可取或不可能的。 - Kerrek SB
2
@111111:你会知道编译器在做什么,因为它是完全指定的。就像默认复制通过复制每个基类和成员来指定一样, 默认移动通过移动每个基类和成员来指定。如果这对于你的类是正确的选择,那么就使用默认值,否则自己编写或禁用它(与处理复制没有区别)。对于像std::string这样的类型,你应该能够依赖它们具有快速的移动构造函数和移动赋值运算符。如果不确定,可以进行测量。有一个很棒的新的<chrono>头文件,使得精确计时非常简单! :-) - Howard Hinnant
1
关于你所做的一些问题:我曾经认为使用传值(像你所做的那样)编写有价值的构造函数(以及setter/有价值赋值)是新的惯用语,但是将参数“移动”到成员中更受欢迎;而且为什么要在这里编写swap?这是否仅仅是为了说明,在那个成员执行更多不在此处显示的操作的情况下?或者说,如果存在不一定具有nothrow移动的成员怎么办?我认为默认的std::swap在这里已经足够了。 - Luc Danton
当我发表之前的评论时,这不是我想到的 :) 你能解释一下为什么你没有在构造函数中使用name(std::move(name_)),以及为什么你要编写一个swap成员吗? - Luc Danton
显示剩余4条评论

3
你是否仔细阅读了错误信息?它发现你有多个赋值运算符和移动赋值运算符,这是完全正确的!
特殊成员必须最多指定一次,无论它们是默认的、删除的、传统定义的还是通过省略隐式处理的。你有两个赋值运算符(一个采用my_type,另一个采用my_type const &)和两个移动赋值运算符(一个采用my_type,另一个采用my_type &&)。请注意,采用my_type的赋值运算符可以处理左值和右值引用,因此它既作为复制赋值,又作为移动赋值。
大多数特殊成员的函数签名有多种形式。你必须选择一个;你不能使用一个不寻常的形式,然后删除传统的形式,因为那将是一个重复声明。编译器将自动使用一个不寻常的特殊成员,并不会合成具有传统签名的特殊成员。
(请注意,错误信息提到了三个候选项。对于每种赋值类型,它看到适当删除的方法、采用my_type的方法,以及其他删除的方法作为紧急接近匹配。)

0

你应该删除那些赋值运算符的重载吗?你的赋值运算符声明不是应该是一个模板或其他什么吗?我真的看不出来那样怎么能工作。

请注意,即使那样操作有效,通过以这种方式实现移动赋值运算符,刚被移出的对象所持有的资源将在其生命周期结束时释放,而不是在赋值点释放。请参考这里获取更多详细信息:

http://cpp-next.com/archive/2009/09/your-next-assignment/


不会,内部数据或R值将被交换到新对象(operator=的参数),并且在释放r值时删除新对象中的空数据。 - 111111

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接