警告:X 的默认移动赋值运算符将多次移动赋值虚拟基类 Y

jww*_*jww 4 c++ multiple-inheritance move-semantics c++11

在 C++11 下测试库时,我在 Clang 下收到警告。我以前从未遇到过警告,搜索并没有提供太多的阅读和研究方式。

警告如下所示,它似乎与多重继承和公共基类有关。但我不清楚触发警告的细节或我应该做些什么来解决它。

我的第一个问题是,这是一个需要解决的问题吗?还是仅仅是效率问题?

我的第二个问题是(如果需要),如何解决警告?或者有哪些可用于修复它的选项?


以下是一些附加信息:

  • 编译器:Apple LLVM version 6.0 (clang-600.0.57)(基于LLVM 3.5svn)
  • g++ -DDEBUG -g2 -O2 -std=c++11 -fPIC -march=native -pipe -c test.cpp

还在 Stack Overflow 上回顾了以下内容,但我不清楚它们在哪里相交:

Crypto++也大量使用Curiously Recurring Template Pattern进行编译时多态性。


头文件可在线获取,这里是实际警告:

g++ -DDEBUG -g2 -O2 -std=c++11  -Wno-deprecated-declarations -fPIC -march=native -pipe -c rsa.cpp
In file included from rsa.cpp:4:
In file included from ./rsa.h:12:
./pubkey.h:635:26: warning: defaulted move assignment operator of 'InvertibleRSAFunction' will move assign virtual base class 'CryptoMaterial' multiple times [-Wmultiple-move-vbase]
class CRYPTOPP_NO_VTABLE TF_ObjectImpl : public TF_ObjectImplBase<BASE, SCHEME_OPTIONS, KEY_CLASS>
                         ^
./rsa.h:57:44: note: 'CryptoMaterial' is a virtual base class of base class 'CryptoPP::RSAFunction' declared here
class CRYPTOPP_DLL InvertibleRSAFunction : public RSAFunction, public TrapdoorFunctionInverse, public PKCS8PrivateKey
                                           ^~~~~~~~~~~~~~~~~~
./rsa.h:57:96: note: 'CryptoMaterial' is a virtual base class of base class 'CryptoPP::PKCS8PrivateKey' declared here
class CRYPTOPP_DLL InvertibleRSAFunction : public RSAFunction, public TrapdoorFunctionInverse, public PKCS8PrivateKey
                                                                                               ^
1 warning generated.
Run Code Online (Sandbox Code Playgroud)

我很抱歉没有减少它。我不确定如何减少它并捕捉警告/投诉的本质。

Jon*_*ely 7

警告对我来说似乎不言自明,它告诉您对派生类型进行移动分配将导致对基进行两次移动分配。

减少它是微不足道的,只需使用一个虚拟基和两条路径来创建一个继承层次结构:

#include <stdio.h>

struct V {
    V& operator=(V&&) { puts("moved"); return *this; }
};

struct A : virtual V { };

struct B : virtual V { };

struct C : A, B { };

int main() {
    C c;
    c = C{};
}
Run Code Online (Sandbox Code Playgroud)

这将打印"moved"两次,因为隐含的举动赋值运算符为每个ABC会做一个按成员分配,这意味着这两个A::operator=(A&&)B::operator=(B&&)将分配的基类。正如艾伦所说,这是标准的有效实现。(该标准规定,在构造时,只有派生程度最高的类型才能构造虚拟基类,但它对赋值没有相同的要求)。

这不是特定于移动分配,将基类更改为仅支持复制分配而不是移动分配将打印"copied"两次:

struct V {
    V& operator=(const V&) { puts("copied"); return *this; }
};
Run Code Online (Sandbox Code Playgroud)

出现这种情况完全一样的原因,既A::operator=(A&&)B::operator=(B&&)将分配的基类。编译器不会针对这种情况发出警告,因为进行两次复制赋值(可能)只是次优的,没有错。对于移动分配,它可能会丢失数据。

如果您的虚拟基础实际上没有任何需要复制或移动的数据,或者只有可简单复制的数据成员,则使其仅支持复制而不移动将抑制警告:

struct V {
    V& operator=(const V&) = default;
};
Run Code Online (Sandbox Code Playgroud)

这个复制赋值运算符仍然会被调用两次,但由于它不做任何事情,所以没有问题。两次什么都不做仍然是什么。

(GCC 在这里似乎比 Clang 聪明一点,如果它是微不足道的,它不会警告虚拟基的移动赋值运算符被调用两次,因为微不足道的移动等效于复制,因此不太可能成为问题)。

如果虚拟基础确实有需要在分配时复制的数据,那么将其复制而不是移动可能仍然是一个不错的选择,但这取决于类型是什么以及做什么。您可能需要在层次结构的每个级别明确定义复制和移动分配。虚拟基地很棘手,很难正确使用,尤其是在复制或移动时。将具有虚基的类型视为可以轻松复制和移动的值类型可能是设计错误。

iostreams 层次结构使用虚拟基础,但已小心且正确地完成。iostream 类型是不可复制的,只能移动,派生类型明确定义移动分配以确保basic_ios<>基类只更新一次。具体来说,basic_iostream::operator=(basic_iostream&&)只在basic_istream基础上运作,而不是basic_ostream一个。上面例子的等价物是:

struct C : A, B {
     C& operator=(C&& c) {
         static_cast<A&>(*this) = static_cast<A&&>(c);
         return *this;
     }
};
Run Code Online (Sandbox Code Playgroud)

直到 C++11,当右值引用和移动语义使有用的语义成为可能时,Iostream 才完全不可复制。如果您的类在 C++03 中一直是可复制的,那么它可能已经是一个有问题的设计,本应该是不可复制的,或者已经仔细编写了复制操作而不是隐式定义的复制操作。

简而言之,每当您拥有虚拟基础时,您都需要非常仔细地考虑构建、分配和销毁的工作方式,以及复制和分配是否对类型有意义。