2

所以玩弄移动语义。

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

 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()在传递参数时使用了我希望它绑定到移动赋值运算符。我究竟做错了什么?

4

4 回答 4

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 中,不能成功地重载参数仅在按值和按引用之间不同的函数。例如:

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,并且在不修改右值引用的情况下进行了扩展:

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.
于 2013-11-07T01:36:18.207 回答
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. 没有副本,只是做一个交换

我们可以写成operator=(String rhs)处理这两种情况的最佳方式。

但是当存在移动赋值运算符时,该反对意见不适用。在可以跳过副本的情况下,operator=(String && rhs)将接管。这就解决了第二种情况。因此,我们只需要实现第一种情况,我们就String(const String &rhs_ref)可以做到这一点。

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

于 2013-11-07T18:17:58.100 回答
0

(很抱歉添加了第三个答案,但我想我终于得到了一个我满意的解决方案。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);

根据需要,此决胜局支持后者,但不幸的是,我们收到一条错误消息:错误:无法分配“字符串”类型的对象,因为它的复制赋值运算符已被隐式删除。

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

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?");.

于 2013-11-07T23:49:18.773 回答
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&),则它不再模棱两可。

我不确定这是编译器中的错误还是我在某处的标准中缺少的东西,但它似乎应该能够区分副本和右值引用之间的区别。

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

于 2013-11-07T01:18:42.260 回答