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

问题描述 投票:3回答:3

请考虑以下简单代码:

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

struct Derived : Base { };

Base foo()
{
  Derived derived;
  return derived;
}

clang 8.0.0 gives a warning -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)

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

例如。看看the code (compiled with the -Wno-return-std-move flag)

#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();
}

它的输出:

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

foo_move
Base move
Derived destruction
Flag: false
Base destruction
Base destruction
c++ clang warnings c++17 move
3个回答
4
投票

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

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

Base foo()
{
  Derived derived;
  return derived;
}

derived是一个自动存储对象,它正在返回 - 它无论如何都要死了,所以从中移动是安全的。所以我们试着这样做 - 我们将它视为右值,我们找到Base(Base&&)。这是一个可行的构造函数,但它需要一个Base&& - 我们需要非常具体的Derived&&。所以最终复制。

但副本很浪费。为什么在derived超出范围时复制?为什么在使用便宜的操作时会使用昂贵的操作?这就是警告的原因,提醒你写一下:

Base foo()
{
  Derived derived;
  return std::move(derived); // ok, no warning
}

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


4
投票

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

您担心的是Derived的析构函数可能会在移动之后访问Base状态,这可能会导致错误。如果确实发生了这样的错误,那是因为Derived的作者犯了一个错误,不是因为用户不应该移动Base子对象。这些错误可以像其他错误一样被发现,并向Derived的作者报告。

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


2
投票

通常建议您的层次结构中唯一不抽象的类应该是叶类。用作多态基类的所有东西都应该是抽象的。

这将使原始代码(其中铿锵声警告)首先是非法的,因为您将无法按值返回Base。事实上,原始代码会给读者留下许多问题,主要是因为它违反了这一准则:

  • 创造一个Derived并且只返回值为Base的重点是什么?
  • 这里发生了什么object slicing吗?如果有人在任何一个类中添加代码,它可能会在将来发生吗?
  • 相关地,如果您的类不是多态的(没有虚拟析构函数,仅举一个问题),您希望如何强制执行类不变量?
  • 为了满足Liskov替换原则,要么所有类型的派生类都允许将Base子对象移出,或者不允许移出它们。在后一种情况下,可以通过删除Base的移动构造函数来防止这种情况。在前一种情况下,警告没有问题。
  • 你的阶级不变量必须如此复杂,以至于摧毁一个Base本身是好的,用它的Derived摧毁Base是好的,但是没有它的Derived摧毁一个Base会不会很好?请注意,如果您遵循rule of zero,这几乎是不可能的。

所以是的,有可能编写使用std::move的代码,因为clang建议改变含义。但是,该代码必须违反许多编码原则。我认为期望编译器警告尊重这种可能性是不合理的。

© www.soinside.com 2019 - 2024. All rights reserved.