请考虑以下简单代码:
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
对于同一层次结构中的对象,-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
}
现在,如果切片对于这个层次结构是错误的,那么即使复制也是做错了,你还有其他问题。但是如果切片是可以接受的,那么你想要移动到这里,而不是复制,而且此刻的语言可以说是错误的。警告是为了帮助确保您做正确的事情。
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
的私有基类,或私有数据成员,而不是公共基类。
通常建议您的层次结构中唯一不抽象的类应该是叶类。用作多态基类的所有东西都应该是抽象的。
这将使原始代码(其中铿锵声警告)首先是非法的,因为您将无法按值返回Base
。事实上,原始代码会给读者留下许多问题,主要是因为它违反了这一准则:
Derived
并且只返回值为Base
的重点是什么?Base
子对象移出,或者不允许移出它们。在后一种情况下,可以通过删除Base
的移动构造函数来防止这种情况。在前一种情况下,警告没有问题。Base
本身是好的,用它的Derived
摧毁Base
是好的,但是没有它的Derived
摧毁一个Base
会不会很好?请注意,如果您遵循rule of zero,这几乎是不可能的。所以是的,有可能编写使用std::move
的代码,因为clang建议改变含义。但是,该代码必须违反许多编码原则。我认为期望编译器警告尊重这种可能性是不合理的。