尝试根据移动分配编写移动构造函数

时间:2013-11-07 01:02:18

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

所以玩弄Move Semantics。

所以我第一次看到这个是这样的:

 class String
 {
     char*   data;
     int     len;
     public:
         // Normal rule of three applied up here.
         void swap(String& rhs) throw()
         {
            std::swap(data, rhs.data);
            std::swap(len,  rhs.len);
         }
         String& operator=(String rhs) // Standard Copy and swap. 
         {
            rhs.swap(*this);
            return *this;
         }

         // New Stuff here.
         // Move constructor
         String(String&& cpy) throw()    // ignore old throw construct for now.  
            : data(NULL)
            , len(0)
         {
            cpy.swap(*this);
         }
         String& operator=(String&& rhs) throw() 
         {
            rhs.swap(*this);
            return *this;
         }
};

看着这个。我可能值得根据Move赋值定义Move构造函数。它具有很好的对称性,我喜欢它,因为它看起来也很干(和复制和交换一样)。

所以我将Move Constructor重写为:

         String(String&& cpy) throw() 
            : data(NULL)
            , len(0)
         {
            operator=(std::move(cpy));
         }

但这会产生歧义错误:

String.cpp:45:9: error: call to member function 'operator=' is ambiguous
        operator=(std::move(rhs));
        ^~~~~~~~~
String.cpp:32:13: note: candidate function
    String& operator=(String rhs)
            ^
String.cpp:49:13: note: candidate function
    String& operator=(String&& rhs) throw()
            ^
1 error generated.

由于我在传递参数时使用std::move(),因此我希望将其绑定到Move赋值运算符。我做错了什么?

4 个答案:

答案 0 :(得分:3)

  

我做错了什么?

您尝试根据另一个特殊成员函数编写一个特殊成员函数的情况应该是罕见的。每个特殊成员通常都需要特别注意。 如果在使每个特殊成员尽可能高效之后,您会看到合并代码的机会,然后,只有这样才能努力。

从在特殊成员之间合并代码的目标开始是错误的开始。

步骤1.首先尝试使用= default编写特殊成员。

步骤2.如果失败,则自定义每个无法用= default写的。

步骤3.编写测试以确认步骤2正在运行。

步骤4.完成第3步后,查看是否可以在不牺牲性能的情况下进行代码整合。这可能涉及编写性能测试。

直接跳到第4步很容易出错,并且经常导致严重的性能损失。

以下是您的示例的第2步:

#include <algorithm>

 class String
 {
     char*   data;
     int     len;
     public:
         String() noexcept
            : data(nullptr)
            , len(0)
            {}

         ~String()
         {
            delete [] data;
         }

         String(const String& cpy)
            : data(new char [cpy.len])
            , len(cpy.len)
         {
            std::copy(cpy.data, cpy.data+cpy.len, data);
         }

         String(String&& cpy) noexcept
            : data(cpy.data)
            , len(cpy.len)
         {
            cpy.data = nullptr;
            cpy.len = 0;
         }

         String& operator=(const String& rhs)
         {
            if (this != &rhs)
            {
                if (len != rhs.len)
                {
                    delete [] data;
                    data = nullptr;
                    len = 0;
                    data = new char[rhs.len];
                    len = rhs.len;
                }
                std::copy(rhs.data, rhs.data+rhs.len, data);
            }
            return *this;
         }

         String& operator=(String&& rhs) noexcept
         {
            delete [] data;
            data = nullptr;
            len = 0;
            data = rhs.data;
            len = rhs.len;
            rhs.data = nullptr;
            rhs.len = 0;
            return *this;
         }

         void swap(String& rhs) noexcept
         {
            std::swap(data, rhs.data);
            std::swap(len,  rhs.len);
         }
};

<强>更新

应该注意的是,在C ++ 98/03中,不能成功地重载参数仅在by-value和by-reference之间不同的函数。例如:

void f(int);
void f(int&);

int
main()
{
    int i = 0;
    f(i);
}

test.cpp:8:5: error: call to 'f' is ambiguous
    f(i);
    ^
test.cpp:1:6: note: candidate function
void f(int);
     ^
test.cpp:2:6: note: candidate function
void f(int&);
     ^
1 error generated.

添加const无效:

void f(int);
void f(const int&);

int
main()
{
    f(0);
}

test.cpp:7:5: error: call to 'f' is ambiguous
    f(0);
    ^
test.cpp:1:6: note: candidate function
void f(int);
     ^
test.cpp:2:6: note: candidate function
void f(const int&);
     ^
1 error generated.

这些相同的规则适用于C ++ 11,并且无需修改即可扩展到rvalue-references:

void f(int);
void f(int&&);

int
main()
{
    f(0);
}

test.cpp:7:5: error: call to 'f' is ambiguous
    f(0);
    ^
test.cpp:1:6: note: candidate function
void f(int);
     ^
test.cpp:2:6: note: candidate function
void f(int&&);
     ^
1 error generated.

因此,给出以下内容并不令人惊讶:

String& operator=(String rhs);
String& operator=(String&& rhs) throw();

结果是:

String.cpp:45:9: error: call to member function 'operator=' is ambiguous
        operator=(std::move(rhs));
        ^~~~~~~~~
String.cpp:32:13: note: candidate function
    String& operator=(String rhs)
            ^
String.cpp:49:13: note: candidate function
    String& operator=(String&& rhs) throw()
            ^
1 error generated.

答案 1 :(得分:2)

我相信必须编写复制构造函数:

     String& operator=(const String &rhs_ref) // (not-so-standard) Copy and Swap. 
     {
        String rhs(rhs_ref); // This is the copy
        rhs.swap(*this);     // This is the swap
        return *this;
     }

在C ++ 03中,对这种方法的反对意见是编译器很难完全优化它。在C ++ 03中,使用operator=(String rhs)很好,因为在某些情况下编译器可以跳过复制步骤并在适当的位置构建参数。例如,即使在C ++ 03中,也可以优化对String s; s = func_that_returns_String_by_value();的调用以跳过副本。

因此,“复制和交换”应重命名为“仅在必要时复制,然后执行交换”。

编译器(在C ++ 03或C ++ 11中)采用以下两种路径之一:

  1. 一个(必要的)副本,然后是交换
  2. 没有副本,只是做一个交换
  3. 我们可以将operator=(String rhs)写为处理这两种情况的最佳方式。

    但是当存在移动赋值运算符时,该异议不适用。在可以跳过副本的情况下,operator=(String && rhs)将接管。这照顾第二种情况。因此,我们只需要实现第一种情况,我们使用String(const String &rhs_ref)来实现这一点。

    它的缺点是需要更多的输入,因为我们必须更明确地进行复制,但我不知道这里缺少任何优化机会。 (但我不是专家......)

答案 2 :(得分:0)

我会把它作为一个答案,所以我可以尝试编写可读的代码来讨论,但我的语义也可能混淆了(所以认为它是一个正在进行的工作):

std::move会返回一个xvalue,但你真的想要一个rvalue,所以在我看来这应该是有效的:

String(String&& cpy) throw() : data(NULL), len(0)
{
    operator=(std::forward<String>(cpy));
    //        ^^^^^^^^^^^^ returns an rvalue 
}

由于std::forward会给你一个右值,而operator=(String&&)期待一个。在我看来,使用而不是std::move

是有意义的

修改

我做了一个小实验(http://ideone.com/g0y3PL)。似乎编译器无法区分String& operator=(String)String& operator=(String&&)之间的差异;但是,如果您将复制赋值运算符的签名更改为String& operator=(const String&),则它不再含糊不清。

我不确定这是编译器中的错误还是我在标准中遗漏的东西,但似乎能够区分副本和左值参考。

总之,霍华德关于不在其他特殊功能方面实施特殊功能的说明似乎是更好的方法。

答案 3 :(得分:0)

(很抱歉添加了第三个答案,但我想我终于得到了一个我满意的解决方案。Demo on ideone

你有一个包含这两种方法的类:

String& operator=(String copy_and_swap);
String& operator=(String && move_assignment);

问题是含糊不清。我们想要一个支持第二种选择的打破平局,因为第二种超载可以在更可行的情况下提高效率。因此,我们用模板化方法替换第一个版本:

template<typename T>
String& operator=(T templated_copy_and_swap);
String& operator=(String && move_assignment);

根据需要,这种争论有利于后者,但遗憾的是我们收到一条错误消息:错误:无法分配“String”类型的对象,因为它的副本赋值运算符被隐式删除。 < / p>

但我们可以解决这个问题。我们需要声明一个复制赋值运算符,这样它就不会决定隐式删除它,但我们还必须确保不再引入歧义。这是一种方法。

const volatile String&& operator=(String&) volatile const && = delete;

现在我们有三个赋值运算符(其中一个是deleted),具有适当的打破平局。请注意volatile const &&。这样做的目的是简单地添加尽可能多的限定符,以便为此过载提供非常低的优先级。并且,如果您尝试分配给volatile const &&的对象,那么您将收到编译器错误,然后您可以处理它。

(使用clang 3.3和g ++ - 4.6.3进行测试,它执行所需的副本和交换次数(即尽可能少!使用g ++,您需要volatile const而不是volatile const &&但是那没关系。)

编辑:类型扣除风险:在模板化operator=的实现中,您可能需要考虑对推导出的类型进行谨慎,例如static_assert( std::is_same<T,String>(), "This should only accept Strings. Maybe SFINAE and enable_if on the return value?");