尝试根据移动赋值编写移动构造函数

Trying to Write Move Constructor in terms of Move Assignment

本文关键字:移动 构造函数 赋值      更新时间:2023-10-16

所以玩移动语义。

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

 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 构造函数。它有一个很好的对称性,我喜欢它,因为它看起来也很干燥(并且喜欢复制和交换)。

所以我将移动构造函数重写为:

         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 赋值运算符。我做错了什么?

我做错了什么?

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

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

第 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.
我相信

必须编写复制构造函数:

     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)来做到这一点。

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

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

std::move返回一个 x值,但您确实想要一个 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&) ,则不再模棱两可。

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

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

(很抱歉添加了第三个答案,但我想我终于得到了一个我满意的解决方案。演示在 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"的对象,因为它的复制赋值运算符被隐式删除。

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

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