8

我的理解是,在 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 中,编译器应该将d1和都d2视为右值,以便在这两个返回语句中进行重载决策。然而,在 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=)

所以这开始是一个工具问题,最后是“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

在巴里的回答之后,在我看来,Clang 3.9+ 在所有情况下都做了技术上正确的事情;GCC 8+在所有情况下都做了理想的事情;总的来说,我应该停止教人们“只是return x让编译器 DTRT”(或者至少用一个巨大的闪烁警告来教它),因为实际上编译器不会DTRT,除非你使用的是前沿(技术上非-符合)GCC。

4

1 回答 1

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 表达式,它命名一个对象,该对象具有在最内层封闭函数或 lambda 表达式的主体或参数声明子句中声明的自动存储持续时间,或
  • [ ... ]

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

在:

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

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

在:

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

我们再次遇到第一个项目符号,但重载解析会发现Base(Base&& ). 所选构造函数的第一个参数不是对(可能是 cv 限定的)的右值引用Derived,因此我们再次执行重载决策 - 并最终复制。

于 2018-02-11T03:07:38.093 回答