【问题标题】:Move Assignment incompatible with Standard Copy and Swap移动分配与标准复制和交换不兼容
【发布时间】:2013-11-19 10:25:00
【问题描述】:

测试新的移动语义。

我刚刚询问了我在使用 Move Constructor 时遇到的问题。但是,在 cmets 中,问题实际上是当您使用标准的“复制和交换”习语时,“移动赋值”运算符和“标准赋值”运算符会发生冲突。

这是我正在使用的课程:

#include <string.h>
#include <utility>

class String
{
    int         len;
    char*       data;

    public:
        // Default constructor
        // In Terms of C-String constructor
        String()
            : String("")
        {}

        // Normal constructor that takes a C-String
        String(char const* cString)
            : len(strlen(cString))
            , data(new char[len+1]()) // Allocate and zero memory
        {
            memcpy(data, cString, len);
        }

        // Standard Rule of three
        String(String const& cpy)
            : len(cpy.len)
            , data(new char[len+1]())
        {
            memcpy(data, cpy.data, len);
        }
        String& operator=(String rhs)
        {
            rhs.swap(*this);
            return *this;
        }
        ~String()
        {
            delete [] data;
        }
        // Standard Swap to facilitate rule of three
        void swap(String& other) throw ()
        {
            std::swap(len,  other.len);
            std::swap(data, other.data);
        }

        // New Stuff
        // Move Operators
        String(String&& rhs) throw()
            : len(0)
            , data(null)
        {
            rhs.swap(*this);
        }
        String& operator=(String&& rhs) throw()
        {
            rhs.swap(*this);
            return *this;
        }
};

我认为非常标准。

然后我像这样测试我的代码:

int main()
{
    String  a("Hi");
    a   = String("Test Move Assignment");
}

这里对a 的赋值应该使用“移动赋值”操作符。但是与“标准分配”运算符(它被写为您的标准副本和交换)存在冲突。

> g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.0.0
Thread model: posix

> g++ -std=c++11 String.cpp
String.cpp:64:9: error: use of overloaded operator '=' is ambiguous (with operand types 'String' and 'String')
    a   = String("Test Move Assignment");
    ~   ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
String.cpp:32:17: note: candidate function
        String& operator=(String rhs)
                ^
String.cpp:54:17: note: candidate function
        String& operator=(String&& rhs)
                ^

现在我可以通过将“标准分配”运算符修改为:

    String& operator=(String const& rhs)
    {
        String copy(rhs);
        copy.swap(*this);
        return *this;
    }

但这并不好,因为它会干扰编译器优化复制和交换的能力。请参阅什么是复制和交换习语? herehere

我是否遗漏了一些不那么明显的东西?

【问题讨论】:

  • 这听起来像是 Apple “假装” C++11 库之一。它不提供该功能。它在过去给我造成了如此多的悲伤,我无法讲述所有的问题。使用__has_feature(cxx_implicit_moves) 保护代码路径。另一个是 Android 上的 STLport。

标签: c++ c++11 move-semantics copy-and-swap


【解决方案1】:

如果将赋值运算符定义为获取值,则不应(不需要也不能)定义赋值运算符以获取右值引用。没有任何意义。

通常,当您需要区分左值和右值时,您只需要提供采用右值引用的重载,但在这种情况下,您选择的实现意味着您不需要进行区分。无论您有左值还是右值,您都将创建参数并交换内容。

String f();
String a;
a = f();   // with String& operator=(String)

在这种情况下,编译器会将调用解析为a.operator=(f());,它会意识到返回值的唯一原因是operator=的参数,并且会忽略任何副本——这就是制作的重点该函数首先取值!

【讨论】:

  • 如果您只有operator =(String),则在从len 小于目标值的左值进行分配时,您将失去性能优化。看我的帖子。
  • @CassioNeri:很好的优化。但我认为它是针对这类问题的(我选择这个作为例子是我的错)。我正在尝试查看一般情况。
  • @CassioNeri:为此,长度和容量必须有两个独立的成员。即使进行了这种更改,并且在一般情况下,例如对于矢量的手工实现,如果您想提供强大的异常保证,您仍然需要将副本放在一边。
  • @CassioNeri:除此之外,你是对的,如果你想要左值和右值的不同行为,那么你就会重载,但是你在左值引用和右值引用上重载,而不是值与任何的引用类型
  • @Troy:分析程序和数据可用吗?我想看看。
【解决方案2】:

其他答案建议只有一个重载 operator =(String rhs) 按值获取参数,但这不是最有效的实现。

在 David Rodríguez 的这个例子中确实如此 - dribeas

String f();
String a;
a = f();   // with String& operator=(String)

没有复制。但是,假设只提供了operator =(String rhs) 并考虑以下示例:

String a("Hello"), b("World");
a = b;

会发生什么

  1. b复制到rhs(内存分配+memcpy);
  2. arhs 互换;
  3. rhs 被销毁。

如果我们实现operator =(const String&amp; rhs)operator =(String&amp;&amp; rhs),那么当目标的长度大于源的长度时,我们可以避免步骤 1 中的内存分配。例如,这是一个简单的实现(不完美:如果String 有一个capacity 成员可能会更好):

String& operator=(const String& rhs) {
    if (len < rhs.len) {
        String tmp(rhs);
        swap(tmp);
    else {
        len = rhs.len;
        memcpy(data, rhs.data, len);
        data[len] = 0;
    }
    return *this;
}

String& operator =(String&& rhs) {
    swap(rhs);
}

除了性能点如果swapnoexcept,那么operator =(String&amp;&amp;) 也可以是noexcept。 (如果内存分配是“潜在”执行的,则情况并非如此。)

在 Howard Hinnant 的这篇出色的 explanation 中查看更多详细信息。

【讨论】:

  • 那么公平地说,当且仅当您不希望编写单独的移动赋值运算符时,复制赋值运算符应该按值获取其参数?
  • @MattMcNabb 恕我直言,这是一个非常合理的指导方针,但有时会发生这种情况,可能存在不适用的情况(我没有任何示例可提供)。
  • 您通常需要小心,但是使用“operator=(Whatever &&rhs)”。当函数退出时“rhs”仍然存在(它的析构函数还没有运行),所以之前存储在目标对象中的资源还没有释放(惊喜!)。如果有人通过“obj1 = std::move(obj2)”调用它,并且“Whatever”拥有某种影响外部世界的资源(独占句柄、锁、大块内存等),那么你应该在退出函数之前显式释放资源。 C++ 充满了绊脚石,我自己在 30 多年后仍然在与它斗争(在 C 和 C++ 中)。
  • @Larry 这毫无意义。如果锁是独占的并且在函数结束时 both 对象持有它,那么你正在做一些疯狂的事情。如果右值仍然拥有一大块内存,为什么需要释放它?无论如何,它的析构函数都会这样做。听起来您正在编写移动运算符,就好像它们是复制运算符一样。
  • @kfsone:两个对象都没有持有它,因为它们只是交换了资源。但这并不重要。问题是,当分配结束时(即尚未销毁),目标对象以前持有的资源在“rhs”内仍然存在。所以用彼得贝克利的话来说,你已经“……飘进了非确定性破坏的地狱”。在这里查看他的文章thbecker.net/articles/rvalue_references/section_01.html(这种情况隐藏在文章的某个地方,但这是一个广为人知的问题)。
【解决方案3】:

复制和分配你只需要这样:

    // As before
    String(const String& rhs);

    String(String&& rhs)
    :   len(0), data(0)
    {
        rhs.swap(*this);
    }

    String& operator = (String rhs)
    {
        rhs.swap(*this);
        return *this;
    }

   void swap(String& other) noexcept {
       // As before
   }

【讨论】:

  • @Raxvan 根据 Howard Hinnant 的出色 explanation,这很有趣,但不是这个类的最佳实现。
  • @CassioNeri 如果字符串有容量的概念,这几乎是一个好点
  • @DieterLücking 确实,如果String 有一个容量成员,则可以实现最佳效果。然而,即使没有它(仅使用len),当源是一个左值,其len 小于目标时,我们也可以获得更好的性能。看我的帖子。
  • @CassioNeri 因此剩下一个设计决策问题:是复制内容并保留(可能)未使用的额外容量还是分配内容和容量(也可能是额外未使用的容量)更好或迭代器范围assign(first last))的赋值?
  • 为什么是 'rhs.swap(*this)' 而不是 'swap(rhs)'? godbolt.org/g/v3M5xy
猜你喜欢
  • 2019-06-24
  • 1970-01-01
  • 2012-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-29
  • 2011-07-01
  • 1970-01-01
相关资源
最近更新 更多