按值复制而不是移动

时间:2017-10-08 07:29:26

标签: c++ c++11

为什么这个程序会调用复制构造函数而不是移动构造函数?

class Qwe {
public:
    int x=0;
    Qwe(int x) : x(x){}
    Qwe(const Qwe& q) {
        cout<<"copy ctor\n";
    }
    Qwe(Qwe&& q) {
        cout<<"move ctor\n";
    }    
};

Qwe foo(int x) {
    Qwe q=42;
    Qwe e=32;
    cout<<"return!!!\n";
    return q.x > x ? q : e;
}

int main(void)
{
    Qwe r = foo(50);
}

结果是:

return!!!
copy ctor

return q.x > x ? q : e;用于禁用nrvo。当我将它包裹在std::move中时,它确实被移动了。但是在&#34; C ++之旅&#34;作者说,当它可用时,必须调用此移动。

我做错了什么?

2 个答案:

答案 0 :(得分:13)

您没有以允许复制/移动省略的方式编写您的功能。移动替换副本的要求如下:

[class.copy.elision]/3

  

在以下复制初始化上下文中,可能会执行移动操作   用来代替复制操作:

     
      
  • 如果return语句中的表达式是(可能带括号的) id-expression ,它使用自动命名对象   储存期限在体内或体内宣布   最里面的封闭函数的 parameter-declaration-clause lambda-expression
  •   
     

重载决策首先选择复制的构造函数   表现为好像该对象是由右值指定的。如果是第一个   重载决议失败或未执行,或者如果类型   所选构造函数的第一个参数不是右值引用   到对象的类型(可能是cv-qualified),重载决策是   再次执行,将对象视为左值。

以上是来自C ++ 17,但C ++ 11的措辞几乎相同。条件运算符不是一个id-expression,用于命名函数范围内的对象。

在您的特定情况下,id-expression类似于qe。您需要命名该范围内的对象。条件表达式不符合命名对象的条件,因此它必须预先形成副本。

如果你想在困难的文本墙上练习你的英语理解能力,那么这就是用C ++ 11编写的。需要付出一些努力才能看到​​IMO,但它与上面澄清的版本相同:

  

当满足某些条件时,允许省略实现   复制/移动类对象的构造,即使复制/移动   对象的构造函数和/或析构函数具有副作用。 [...]   复制/移动操作的省略,称为复制省略,是   在下列情况下允许(可以合并到   消除多份副本):

     
      
  • 在具有类返回类型的函数的return语句中,当表达式是非易失性自动对象的名称时(其他   比函数或catch子句参数)与之相同   cv-unqualified type作为函数返回类型,copy / move   通过直接构造自动对象可以省略操作   进入函数的返回值
  •   
     

当符合或将要执行复制操作的标准时   因为源对象是一个函数参数这个事实,   并且要复制的对象由左值,超载指定   首先执行选择复制的构造函数的分辨率   好像对象是由右值指定的。如果超载分辨率   失败,或者如果所选的第一个参数的类型   构造函数不是对象类型的右值引用(可能   cv-qualified),重新执行重载决策,考虑到   对象作为左值。

答案 1 :(得分:0)

StoryTeller没有回答这个问题:为什么不采取行动? (而不是:为什么没有复制品?)

我在这里:当且仅在以下情况下才会调用此举:

  • 不执行复制省略(RVO)。您对三元运算符的使用确实是一种防止复制省略的方法。让我指出,如果您只想在抑制复制省略时返回return (0, q);q是一种更简单的方法。这使用(in-)着名的逗号运算符。可能return ((q));也可能有效,但我还不足以说明语言律师。
  • return的参数是右值。这可能是暂时的(更准确地说,是prvalue),但这些也有资格获得复制。因此,return的参数必须是xvalue,例如std::move(q),如果您想确保调用此移动c。

另请参阅:C++ value categories

您的示例的更多技术细节:

  • qeQwe类型的对象。
  • q.x > x ? q : eQwe类型的左值表达式。这是因为表达式 qeQwe类型的左值。三元运算符只选择其中任何一个。
  • std::move(q.x > x ? q : e)Qwe类型的xvalue表达式。 std::move只是将左值转换(强制转换)为x值。另外,q.x > x ? std::move(q) : std::move(e)也可以。
  • 副本c在return q.x > x ? q : e;中被调用,因为它可以使用类型Qwe的左值调用(常量是可选的),而另一方面,移动c&# 39; tor不能用左值调用,因此从候选集中删除。

更新:通过更深入地解决评论......这是C ++的一个令人困惑的方面!

从概念上讲,在C ++ 98中,按值返回对象意味着返回对象的副本,因此将调用副本c。tor。但是,标准的作者认为编译器应该可以自由地执行优化,以便在适当的情况下可以省略这种可能昂贵的副本(例如容器)。

此复制省略意味着,不是在一个位置创建对象,然后将其复制到调用者控制的内存地址,被调用者直接在调用者控制的内存中创建对象。因此,&#34;正常&#34;构造函数,例如一个默认的c被称为。

因此,他们添加了一个段落,以便编译器 required 来检查副本c - tor - 无论是生成的还是用户定义的 - 存在且可访问(目前还没有概念)删除的功能),并且必须确保对象初始化 as-if 它首先在不同的地方创建然后复制(参见as-if规则),但是编译器不需要确保复制c的任何副作用都是可观察的,例如示例中的流输出。

仍然要求c&tor; tor之所以存在,是因为他们希望避免编译器能够接受另一个必须拒绝的代码的情况,因为前者实现了可选的优化,后者没有。

在C ++ 11中,添加了移动语义,委员会非常希望以这样的方式使用它,即许多现有的按值返回函数,例如:涉及字符串或容器会变得更有效率。这样做的方式是,在这种情况下,编译器实际上需要执行移动而不是复制。然而,复制省略的想法仍然很重要,所以基本上现在有四种不同的类别:

  1. 编译器需要检查可用的(见上文)移动c,但是允许它忽略它。
  2. 编译器需要检查可用的移动c,并且必须调用它。
  3. 编译器需要检查可用的副本c,但是可以忽略它。
  4. 编译器需要检查可用的副本c,以及必须调用它。
  5. ......这反过来导致四种可能的结果:

    1. 编译器检查移动c,但后来忽略了它。 (与上述有关)
    2. 编译器检查移动c&tor,并实际发出呼叫。 (涉及1. 2.以上)
    3. 编译器检查副本c,但随后将其删除。 (与上述3.有关)
    4. 编译器检查副本c&tor,并实际发出一个调用。 (涉及3. 4.以上)
    5. 长期优化的故事并没有在这里结束,因为在C ++ 17中,编译器是必需的来消除某些c&#tor调用。在这些情况下,甚至不允许编译器要求复制或移动文件可用。

      请注意,在as-if规则的保护下,编译器一直可以自由地忽略那些不符合标准要求的c调用,例如通过函数内联和以下优化步骤。无论如何,从概念上讲,函数调用不必由用于执行子例程的实际机器指令支持。不允许编译器删除可观察的,否则定义的行为。

      到目前为止你应该注意到,至少在C ++ 17之前,同样格式良好的程序很可能表现不同,具体取决于所使用的编译器甚至优化设置, if 副本rsp。移动构造函数具有可观察到的副作用。此外,实现复制/移动省略的编译器可以针对标准允许其发生的条件的子集执行此操作。这使您的问题几乎无法详细回答。为什么复制/移动c在此处调用,但不在那里?好吧,这可能是因为C ++标准的要求,但也可能是您的编译器的偏好。也许编译器作者有时间和闲暇实现一个优化而不是另一个。也许他们发现在后一种情况下太难了。也许他们只是有更重要的事情要做。谁知道?

      对于我来说,作为开发人员,99%的时间是以编译器可以应用最佳优化的方式编写代码。坚持常见案例和标准做法是一回事。知道临时工的NRVO和RVO是另一回事,并且编写代码使得标准允许(或者,在C ++ 17中,需要)复制/移动省略,并确保移动c在其所在的地方可用有益(如果没有发生elision)。不要依赖副作用,例如写日志消息或增加全局计数器。除了可能的调试或学术兴趣之外,这些副本或移动c通常应该做什么。