如果对象处于同一层次结构,-Wreturn-std-move clang 警告是否正确?

Con*_*tor 4 c++ warnings move clang c++17

考虑以下简单代码:

struct Base
{
  Base() = default;      
  Base(const Base&);      
  Base(Base&&);
};

struct Derived : Base { };

Base foo()
{
  Derived derived;
  return derived;
}
Run Code Online (Sandbox Code Playgroud)

clang 8.0.0 对此发出警告-Wreturn-std-move

prog.cc:21:10: warning: local variable 'derived' will be copied despite being returned by name [-Wreturn-std-move]
  return derived;
         ^~~~~~~
prog.cc:21:10: note: call 'std::move' explicitly to avoid copying
  return derived;
         ^~~~~~~
         std::move(derived)
Run Code Online (Sandbox Code Playgroud)

但是如果std::move在这里调用代码的行为可能会改变,因为Base对象的子Derived对象将在调用Derived对象的析构函数之前移动,而最后一个的代码将表现不同。

例如看代码(用-Wno-return-std-move标志编译)

#include <iostream>
#include <iomanip>

struct Base
{
  bool flag{false};

  Base()
  {
    std::cout << "Base construction" << std::endl;
  }

  Base(const bool flag) : flag{flag}
  {
  }

  Base(const Base&)
  {
    std::cout << "Base copy" << std::endl;
  }

  Base(Base&& otherBase)
  : flag{otherBase.flag}
  {
    std::cout << "Base move" << std::endl;
    otherBase.flag = false;
  }

  ~Base()
  {
    std::cout << "Base destruction" << std::endl;
  }
};

struct Derived : Base
{
  Derived()
  {
    std::cout << "Derived construction" << std::endl;
  }

  Derived(const bool flag) : Base{flag}
  {
  }

  Derived(const Derived&):Base()
  {
    std::cout << "Derived copy" << std::endl;
  }

  Derived(Derived&&)
  {
    std::cout << "Derived move" << std::endl;
  }

  ~Derived()
  {
    std::cout << "Derived destruction" << std::endl;
    std::cout << "Flag: " << flag << std::endl;
  }
};

Base foo_copy()
{
  std::cout << "foo_copy" << std::endl;
  Derived derived{true};
  return derived;
}

Base foo_move()
{
  std::cout << "foo_move" << std::endl;
  Derived derived{true};
  return std::move(derived);
}

int main()
{
  std::cout << std::boolalpha;
  (void)foo_copy();
  std::cout << std::endl;
  (void)foo_move();
}
Run Code Online (Sandbox Code Playgroud)

它的输出:

foo_copy
Base copy
Derived destruction
Flag: true
Base destruction
Base destruction

foo_move
Base move
Derived destruction
Flag: false
Base destruction
Base destruction
Run Code Online (Sandbox Code Playgroud)

Bar*_*rry 5

如果对象位于同一层次结构中,-Wreturn-std-move clang 警告是否正确?

是的,警告是正确的。当前的自动移动规则仅在重载解析找到一个采用该类型的右值引用的构造函数时才会发生。在这个片段中:

Base foo()
{
  Derived derived;
  return derived;
}
Run Code Online (Sandbox Code Playgroud)

derived是一个正在返回的自动存储对象 - 它无论如何都会消亡,因此可以安全地移动。所以我们尝试这样做 - 我们将其视为右值,然后我们发现Base(Base&&)。这是一个可行的构造函数,但它需要一个- 并且我们非常具体地Base&&需要一个. 所以最后还是抄袭吧。Derived&&

但副本很浪费。derived无论如何,为什么要在超出范围时进行复制呢?当你可以使用便宜的操作时,为什么要使用昂贵的操作呢?这就是为什么会有警告,提醒你写:

Base foo()
{
  Derived derived;
  return std::move(derived); // ok, no warning
}
Run Code Online (Sandbox Code Playgroud)

现在,如果该层次结构的切片是错误的,那么即使复制也会做错误的事情,并且您还会遇到其他问题。但如果切片是可以接受的,那么你想要移动到这里,而不是复制,而目前的语言可以说是做错了事情。该警告旨在帮助确保您做正确的事情。


在 C++20 中,由于P1825(相关部分来自P1155),原始示例实际上会执行隐式移动。


Bri*_*ian 5

Clang 的警告当然是正确的。由于derivedis 的类型与函数的返回类型不同,因此在语句 中return derived;,编译器必须将其derived视为左值,并且会发生副本。并且可以通过写入来避免此副本return std::move(derived);,将其显式转换为右值。该警告不会告诉您是否应该这样做。它只是告诉你你正在做的事情的后果std::move,以及使用 的后果,让你自己做决定。

您担心的是, 的析构函数Derived可能会Base在移出状态后访问该状态,这可能会导致错误。如果确实发生了这样的错误,那是因为作者Derived犯了错误,而不是因为用户不应该移动Base子对象。此类错误可以通过与其他错误相同的方式发现,并报告给Derived.

我为什么要这样说?因为当作者创建Base公共基类时Derived,他们向用户承诺他们有权Base在与Derived对象交互时使用完整的界面,包括从对象中移动。因此, 的所有成员函数都Derived必须准备好处理用户可能以界面允许的Base任何方式修改子对象的事实Base。如果这不是所希望的,那么Base可以将其设为 的私有基类Derived或私有数据成员,而不是公共基类。