【问题标题】:Is -Wreturn-std-move clang warning correct in case of objects in the same hierarchy?如果对象位于同一层次结构中,-Wreturn-std-move clang 警告是否正确?
【发布时间】:2019-04-30 15:33:45
【问题描述】:

考虑以下简单代码:

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,代码的行为可能会改变,因为Derived 对象的Base 子对象将在调用Derived 对象的析构函数和代码之前移动last 的行为会有所不同。

例如看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

【问题讨论】:

  • 从一个对象移动应该让它处于“有效但未指定的状态”。这意味着通常的操作(例如销毁)应该可以工作。如果他们不这样做,您可能应该删除移动构造函数以避免一起移动。
  • 如果移动确实给您带来了问题,您应该重新考虑设计。 Derived 真的不应该关心它的基础对象是否被移出。
  • @nwp Base Derived 对象的子对象在移动后处于“有效但未指定的状态”。但Derived 对象的状态可能基于其Base 子对象的状态。如果最后一个更改而没有对第一个进行相应更改,则代码的行为可能会有所不同甚至不正确。
  • 我认为不可能编写显示您遇到的问题的“好”代码。依赖复制/移动构造函数的副作用本身已经非常脆弱(由于允许改变可观察行为的各种省略)。而且,如果您本身不依赖副作用,但移出基类会破坏您的类不变量,则不应允许移动基类。

标签: c++ clang warnings c++17 move


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

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

    是的,警告是正确的。当前的自动移动规则只有在重载决议找到一个构造函数时才会发生,特别是,以及对该类型的右值引用。在这个 sn-p 中:

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

    derived 是一个正在被返回的自动存储对象 - 无论如何它都会死去,所以它是安全的。所以我们尝试这样做——我们把它当作一个右值,我们找到Base(Base&amp;&amp;)。这是一个可行的构造函数,但它需要一个Base&amp;&amp; - 我们需要非常具体一个Derived&amp;&amp;。所以最终抄袭。

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

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

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


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

    【讨论】:

    • 您所写的一切都很好且正确,但是在移动后,Base 子对象的 Derived 对象的销毁问题仍处于某种有效但未指定的状态。 Derived销毁的逻辑可能基于Base内部状态,如果改变了但Derived成员没有相应改变,可能会出现奇怪的情况。
    • @Constructor 然后(a)你的移动构造函数不应该移动,(b)你应该禁止切片,因为它可能天生就有问题(也许Base不应该是公开的Base ?) 或 (c) 两者兼而有之。
    • @Constructor 你能提供一个非人为的例子吗?关于切片,我的回答措辞不那么严格,因为从技术上讲你可以想出这样一个例子,但 Barry 在这里指出了这一点——你的班级要么有被切片的问题,要么没有。跨度>
    • 据我从cppreference 了解到,切片案例在 C++20 中将按预期工作。
    【解决方案3】:

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

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

    • 创建Derived 并仅按值返回Base 的意义何在?

    • 这里有object slicing吗?如果有人向任一类添加代码,将来会不会发生?

    • 与此相关,如果您的类不是多态的(仅举一个问题,没有虚拟析构函数),您希望如何强制执行您的类不变量?

    • 为了满足 Liskov 替换原则,所有类型的派生类都应该允许它们的 Base 子对象被移出,或者它们都不被移出。在后一种情况下,可以通过删除Base 的移动构造函数来防止这种情况。在前一种情况下,警告没有问题。

    • 你的类不变量有多么令人费解,所以销毁 Base 本身就可以了,销毁带有 BaseDerived 很好,但是销毁没有 @ 的 Derived 987654334@不行吗?请注意,如果您关注rule of zero,这几乎是不可能的。

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

    【讨论】:

    • 还值得一提的是,如果 Base 删除了其移动构造函数,则不会触发警告,如果从 Base 移动可能违反其不变量或其派生类的不变量,则应该触发。跨度>
    • "所有用作基类的东西都应该是抽象的。" 如果您出于多态原因使用继承,这只是一个好建议。在 C++ 中,继承还有其他用途。
    • @NicolBolas 是的,但是其中有多少涉及卷积类不变量,可以使用切片的 Base 副本,但不能使用 Base-moved-from Derived?或者更确切地说,其中有多少甚至具有非默认析构函数?无论如何,我已经修改了我的答案,谢谢!
    猜你喜欢
    • 2022-01-23
    • 2012-10-10
    • 2020-06-07
    • 1970-01-01
    • 2022-10-27
    • 2021-11-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多