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

Quu*_*one 8 c++ language-lawyer compiler-specific move-semantics c++17

我的理解是,在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!
}
Run Code Online (Sandbox Code Playgroud)

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

此外,GCC和Clang似乎都在这个领域有混乱和可能的错误行为.在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=)
Run Code Online (Sandbox Code Playgroud)

所以这开始是一个工具问题,并最终得到了" 对于C++编译器来说什么是正确的行为?"

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

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

以巴里的答案为灵感,这是一个更完整的测试.我们测试了六种不同的情况,其中需要进行左值到右值的转换.

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
Run Code Online (Sandbox Code Playgroud)

在巴里的回答之后,在我看来,Clang 3.9+在所有情况下都做了技术上正确的事情; GCC 8+ 在所有情况下都做得很好 ; 一般来说,我应该停止教导人们"只是return x让编译器DTRT"(或者至少教它一个巨大的闪烁警告)因为在实践中编译器不会 DTRT,除非你使用的是前沿(并且在技术上不是 - 合规)海湾合作委员会.

Bar*_*rry 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表达式的body或parameter-declaration-clause中声明的自动存储持续时间,或者
  • [...]

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

在:

Unrelated test1() {
    Derived d1;
    return d1;
}
Run Code Online (Sandbox Code Playgroud)

我们遇到了第一个子弹,所以我们尝试Unrelated使用类型的右值进行复制初始化Derived,这给了我们Unrelated(Derived&& ).这符合突出显示的标准,因此我们使用它,结果就是一个举动.

在:

Base test2() {
    Derived d2;
    return d2;  // yes, this is slicing!
}
Run Code Online (Sandbox Code Playgroud)

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