Is -Wreturn-std-move clang warning correct in case of objects in the same hierarchy?

穿精又带淫゛_ 提交于 2020-05-15 04:20:52

问题


Consider the following simple code:

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 on it:

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)

But if one called std::move here the behavior of the code might change because the Base subobject of the Derived object would be moved before the calling of the destructor of the Derived object and the code of the last would behave differently.

E.g. look at 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();
}

Its output:

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

foo_move
Base move
Derived destruction
Flag: false
Base destruction
Base destruction

回答1:


Clang's warning certainly is correct. Since derived is of a type that differs from the function's return type, in the statement return derived;, the compiler must treat derived as an lvalue, and a copy will occur. And this copy could be avoided by writing return std::move(derived);, making it into an rvalue explicitly. The warning doesn't tell you whether or not you should do this. It just tells you the consequences of what you're doing, and the consequences of using std::move, and lets you make up your own mind.

Your concern is that the destructor of Derived might access the Base state after it has been moved from, which might cause bugs. If such bugs do occur, it is because the author of Derived has made a mistake, not because the user should not have moved the Base subobject. Such bugs could be discovered in the same ways as other bugs, and reported to the author of Derived.

Why do I say this? Because when the author made Base a public base class of Derived, they were promising to the user that they are entitled to use the full Base interface whenever interacting with a Derived object, which includes moving from it. Thus, all member functions of Derived must be prepared to deal with the fact that the user may have modified the Base subobject in any way that Base's interface allows. If this is not desired, then Base could be made a private base class of Derived, or a private data member, rather than a public base class.




回答2:


Is -Wreturn-std-move clang warning correct in case of objects in the same hierarchy?

Yes, the warning is correct. The current rules for automatic move only happen if overload resolution finds a constructor that takes, specifically, and rvalue reference to that type. In this snippet:

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

derived is an automatic storage object that is being returned - it's dying anyway, so it's safe to move from. So we try to do that - we treat it as an rvalue, and we find Base(Base&&). That's a viable constructor, but it takes a Base&& - and we need very specifically a Derived&&. So end up copying.

But the copy is wasteful. Why copy when derived is going out of scope anyway? Why use the expensive operation when you can use the cheap one? That's why the warning is there, to remind you to write:

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

Now, if slicing is wrong for this hierarchy, then even copying is doing the wrong thing anyway and you have other problems. But if slicing is acceptable, then you want to move here, not copy, and the language at the moment arguably does the wrong thing. The warning is there to help make sure you do the right thing.


In C++20, the original example would actually perform an implicit move due to P1825 (the relevant part comes from P1155).




回答3:


It is commonly advised that the only classes in your hierarchy that are not abstract should be the leaf classes. Everything that is used as polymorphic base class should be abstract.

  • why derive from a concrete class is a poor design

  • http://ptgmedia.pearsoncmg.com/images/020163371x/items/item33.html

This would make the original code (which clang warns about) illegal in the first place, as you would be unable to return a Base by value. And indeed, the original code leaves many questions open to a reader, primarily because it violates this guideline:

  • What is the point of creating a Derived and only returning a Base by value?

  • Is there any object slicing happening here? Will it maybe happen in the future if someone adds code to either class?

  • Relatedly, how do you want to enforce your class invariants if your classes are not polymorphic (no virtual destructors, to name one issue)?

  • To satisfy the Liskov Substitution Principle, either all kinds of derived classes should allow having their Base subobjects moved out, or none of them. In the latter case, this can be prevented by deleting Base's move constructor. In the former case, there is no issue with the warning.

  • How convoluted would your class invariants have to be so that destroying a Base by itself is fine, destroying a Derived with its Base present is fine, but destroying a Derived without its Base would not be fine? Note that this is pretty much impossible if you follow the rule of zero.

So yes, it is possible to write code where using std::move as clang suggests changes the meaning. But that code would have to violate a lot of coding principles already. I don't think it is reasonable to expect the compiler warnings to respect that possibility.



来源:https://stackoverflow.com/questions/55924330/is-wreturn-std-move-clang-warning-correct-in-case-of-objects-in-the-same-hierar

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!