自动检测C ++ 14"返回应该使用std :: move"情况

时间:2018-02-11 02:47:17

标签: c++ language-lawyer move-semantics c++17 compiler-specific

我的理解是,在C ++ 17中,以下代码段旨在做正确的事:

struct Instrument;  // instrumented (non-trivial) move and copy operations

struct Base {
    Instrument i;
};

struct Derived : public Base {};

struct Unrelated {
    Instrument i;
    Unrelated(const Derived& d): i(d.i) {}
    Unrelated(Derived&& d): i(std::move(d.i)) {}
};

Unrelated test1() {
    Derived d1;
    return d1;
}

Base test2() {
    Derived d2;
    return d2;  // yes, this is slicing!
}

也就是说,在C ++ 17中,为了在这两个return语句中重载解析的目的,编译器应该将d1d2都视为rvalues。但是,在C ++ 14及更早版本中,情况并非如此; return操作数中的左值到右值变换只有在操作数完全正确的返回类型时才适用。

此外,GCC和Clang似乎在这方面都有混乱和可能有错误的行为。 Trying the above code on Wandbox,我看到了这些输出:

GCC 4.9.3 and earlier: copy/copy (regardless of -std=)
Clang 3.8.1 and earlier: copy/copy (regardless of -std=)
Clang 3.9.1 and later: move/copy (regardless of -std=)
GCC 5.1.0 through 7.1.0: move/copy (regardless of -std=)
GCC 8.0.1 (HEAD): move/move (regardless of -std=)

所以这开始是一个工具问题,最终得到了#34的副订单; C ++编译器的正确的行为到底是什么?"

我的工具问题是:在我们的代码库中,我们有几个地方说return x;,但由于我们的工具链是GCC 4.9.x和/或Clang,因此意外生成副本而不是移动。我们想自动检测这种情况,并根据需要插入std::move()。有没有简单的方法来检测这个问题?也许我们可以启用一个铿锵有力的支票或-Wfoo标志?

但是现在我现在也想知道C ++编译器在此代码上的正确行为是什么。这些输出是否表明GCC / Clang错误?他们正在接受工作吗?语言版本(-std=)应该重要吗? (我认为它应该是重要的,除非通过缺陷报告更新了正确的行为,一直回到C ++ 11。)

巴里的回答启发了{p> Here is a more complete test。我们测试了六种不同的情况,其中lvalue-to-rvalue转换是可取的。

GCC 4.9.3 and earlier:   elided/copy/copy/copy/copy/copy
Clang 3.8.1 and earlier: elided/copy/copy/copy/copy/copy
Clang 3.9.1 and later:   elided/copy/move/copy/copy/copy
GCC 5.1.0 through 7.1.0: elided/copy/move/move/move/move
GCC 8.0.1 (HEAD):        elided/move/move/move/move/move

ICC 17:                  elided/copy/copy/copy/copy/copy  
ICC 18:                  elided/move/move/move/copy/copy
MSVC 2017 (wow):         elided/copy/move/copy/copymove/copymove

在Barry的回答之后,在我看来Clang 3.9+在所有情况下都做了技术上正确的事情; GCC 8+在所有情况下都做了理想的事情;一般来说,我应该停止教导人们只需return x并让编译器DTRT" (或者至少用一个巨大的闪烁警告来教它)因为在实践中编译器将 DTRT,除非你使用的是尖端的(并且在技术上不符合要求的)GCC。

1 个答案:

答案 0 :(得分:10)

正确的行为是移动/复制。你可能只想写一个铿锵有力的支票。

C ++ 17中的措辞是[class.copy.elision]/3,而C ++ 14中的措辞是[class.copy]/32。具体的单词和格式不同,但规则是相同的。

在C ++ 11中,规则措辞为[class.copy]/32并且与复制省略规则相关联,自动存储局部变量的例外在CWG 1579中作为缺陷报告添加。在此缺陷报告出现之前的编译器将表现为复制/复制。但是由于缺陷报告针对C ++ 11,实现措辞更改的编译器将在所有标准版本中实现它。

使用C ++ 17措辞:

  

在以下复制初始化上下文中,可能会使用移动操作而不是复制操作:

     
      
  • 如果return语句中的表达式是一个(可能带括号的)id-expression,它指定一个对象,该对象具有在最内层封闭函数或lambda-expression的body或parameter-declaration-clause中声明的自动存储持续时间,或者
  •   
  • [...]
  •   
     首先执行

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

在:

Unrelated test1() {
    Derived d1;
    return d1;
}

我们遇到了第一个项目符号,因此我们尝试使用Unrelated类型的左值复制初始化Derived,这样我们就会Unrelated(Derived&& )。这符合突出显示的标准,因此我们使用它,结果就是一个动作。

在:

Base test2() {
    Derived d2;
    return d2;  // yes, this is slicing!
}

我们再次遇到第一颗子弹,但重载决议会找到Base(Base&& )。所选构造函数的第一个参数是Derived的rvalue引用(可能是cv-qualified),因此我们再次执行重载决策 - 并最终复制。