【问题标题】:Trying to Write Move Constructor in terms of Move Assignment尝试根据移动赋值编写移动构造函数
【发布时间】:2013-11-18 12:20:37
【问题描述】:

所以玩移动语义。

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

 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(),因此我希望它绑定到移动赋值运算符。我做错了什么?

【问题讨论】:

  • 不过,使用尚不存在的对象的赋值运算符并不卫生。
  • throw() 已弃用,取而代之的是 noexcept btw
  • 我的语义可能混淆了,但String&& 不应该是右值吗?所以调用std::move 会尝试将其转换为一个xvalue。你不应该只能打电话给operator=(std::forward(cpy))吗?
  • 你提到了三法则。这是否意味着您在别处定义了String::~String()String::String(const String &input)?这给了我们三分之二,你有你的副本和交换String& String::operator=(...)
  • operator=(String& )operator=(String&& ) 可以通过右值参考与左值参考来消除歧义;但不是operator=(String )operator=(String&& )

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


【解决方案1】:

我做错了什么?

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

以在特殊成员之间整合代码为目标是错误的起点。

第 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.

【讨论】:

  • 感谢您的建议。这正是我所做的(并在我的问题中显示(让它工作然后试图让它干燥))。我现在有一个调用operator=(std::move(rhs)),我希望绑定到移动赋值运算符,但它没有绑定。 PS。学习复制和交换习语。
  • 我完全不同意It should be a rare occurrence that you try to write one special member function in terms of another。复制和交换习语根据Copy Constructor 定义operator=
  • @LokiAstari:我非常了解复制和交换习语。我学到的一些内容在这里介绍:stackoverflow.com/a/18303787/576911。 PS:关于你的其他问题(stackoverflow.com/q/19841626/576911)。重载规则没有什么特别之处,因为它们适用于移动赋值运算符或右值引用。您不能重载按值和按引用。不在 C++98 中,不在 C++03 中,不在 C++11 中。
  • @LokiAstari 我发现这个关于 c++11 的复制和交换的讨论很有帮助:stackoverflow.com/questions/12922138/…
  • @HowardHinnant:您应该将评论的最后一部分添加到您的答案中,这非常重要,人们不需要深入研究 cmets。
【解决方案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 &amp;&amp; rhs) 将接管。这就解决了第二种情况。因此,我们只需要实现第一种情况,我们使用String(const String &amp;rhs_ref)来实现。

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

【讨论】:

  • .. 再三考虑,我认为这里可能缺少一种优化可能性。使用operator(String rhs),调用函数可以立即看到复制将要发生(假设它是必需的)。假设复制构造函数是可内联的,并且如果源数据已经在堆栈上,这一切都可以被优化。所以我认为鼓励编译器内联operator=(const String &amp;rhs_ref)的内容是可取的。有什么想法吗?
  • 我刚刚在这个问题上发布了另一个answer,它展示了一种更简单的解决歧义的方法。感谢 Andy 的反馈。
【解决方案3】:

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

std::move 返回一个 xvalue,但您确实想要一个 rvalue,所以在我看来这应该可以代替:

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

因为std::forward 会给你一个右值,而operator=(String&amp;&amp;) 期待一个。在我看来,使用而不是 std::move 是有意义的。

编辑

我做了一个小实验 (http://ideone.com/g0y3PL)。看来编译器无法区分String&amp; operator=(String)String&amp; operator=(String&amp;&amp;);但是,如果您将复制赋值运算符的签名更改为String&amp; operator=(const String&amp;),它就不再有歧义了。

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

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

【讨论】:

  • 我觉得forward必须叫forward&lt;T&gt;,我不相信它可以做模板推导。此外,它通常仅在完美转发的情况下才有意义。在这种情况下,move 是正确的方法。
  • 我认为你在如何调用forward 方面是正确的,但我不确定我是否在move 部分关注你。 std::move 返回 std::remove_reference&lt;T&gt;::type&amp;&amp;,而 std::forward 返回 T&amp;&amp;。如果您想调用operator=(String&amp;&amp;),您似乎应该将它传递给您想要的String&amp;&amp;(而不是您已经取消引用的右值引用)?我错过了什么?
  • 这取决于我猜T 是什么。我们必须做forward&lt;String&gt;forward&lt;String&amp;&gt;forward&lt;String&amp;&amp;&gt;。如果你使用T=String,那么无论哪种方式,返回值都是相同的类型,所以我想没关系。只有T=String&amp;,forward 的返回类型才会是String&amp;&amp; 以外的任何类型。所以move(..)forward&lt;String&gt;(..) 会做同样的(正确的)事情。
  • 我同意检测StringString&amp;&amp; 之间的差异的困难(和挫败感!)。我最近希望找到一个 type_trait 可以判断一个类型有一个移动构造函数。但出于类似的原因,这样的事情是不可能的。很多人都问过,没有成功!
  • 我想为我的标准赋值运算符使用复制和交换 idium,这就是为什么签名是 String&amp; operator=(String)
【解决方案4】:

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

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

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

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

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

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

编辑:类型推导风险:在模板化 operator= 的实现中,您可能需要考虑小心推导类型,例如 static_assert( std::is_same&lt;T,String&gt;(), "This should only accept Strings. Maybe SFINAE and enable_if on the return value?");

【讨论】:

    猜你喜欢
    • 2015-03-03
    • 2020-03-22
    • 1970-01-01
    • 2016-05-19
    • 1970-01-01
    • 2021-05-31
    • 1970-01-01
    • 2017-01-16
    • 2020-12-03
    相关资源
    最近更新 更多