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

Warning: defaulted move assignment operator of X will move assign virtual base class Y multiple times

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

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

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

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


这里有一些附加信息:

还在 Stack Overflow 上查看了以下内容,但我不清楚它们的交叉点:

用于编译时多态性的库,Crypto++, also make heavy use of Curiously Recurring Template Pattern


The the header file 在线可用,这里是实际警告:

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.

很抱歉没有减少它。我不确定如何减少它并捕捉 warning/complaint.

的本质

该标准允许实现选择一种简单但有时会出错的方法来处理存在虚拟基的情况下的成员分配。

http://en.cppreference.com/w/cpp/language/move_assignment:

As with copy assignment, it is unspecified whether virtual base class subobjects that are accessible through more than one path in the inheritance lattice, are assigned more than once by the implicitly-defined move assignment operator.

这对于移动分配尤其不利,因为它可能意味着从已经 moved-from 的成员分配。

警告对我来说似乎是 self-explanatory,它告诉你 move-assigning 派生类型将导致 move-assigning 基数两次。

减少它很简单,只需使用虚拟基础和两条路径创建继承层次结构即可:

#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{};
}

这将打印 "moved" 两次,因为每个 ABC 的隐式移动赋值运算符将执行成员赋值,这意味着A::operator=(A&&)B::operator=(B&&) 都将分配基数 class。正如艾伦所说,这是该标准的有效实施。 (标准规定在构造时只有 most-derived 类型会构造虚拟基,但它对赋值没有相同的要求)。

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

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

发生这种情况的原因完全相同,A::operator=(A&&)B::operator=(B&&) 都将分配基数 class。编译器不会针对这种情况发出警告,因为进行两次复制赋值(可能)只是次优的,并没有错。对于 move-assignment 它可能会丢失数据。

如果你的虚拟库实际上没有任何需要复制或移动的数据,或者只有可以简单复制的数据成员,那么让它只支持复制不移动将抑制警告:

struct V {
    V& operator=(const V&) = default;
};

这个复制赋值运算符仍然会被调用两次,但是因为它什么都不做所以没有问题。两次什么都不做还是什么都没有。

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

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

iostreams 层次结构使用虚拟基础,但做得很仔细且正确。 iostream 类型是 non-copyable,只能移动,派生类型明确定义移动分配以确保 basic_ios<> 基 class 只更新一次。具体来说,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;
     }
};

Iostream 在 C++11 之前根本不可复制,当时右值引用和移动语义使得处理有用的语义成为可能。如果你的 class 在 C++03 中一直是可复制的,它可能已经是一个有问题的设计,应该是 non-copyable,或者已经仔细编写了复制操作而不是 implicitly-defined。

简而言之,任何时候你有虚拟基础时,你都需要非常仔细地考虑构造、赋值和销毁的工作方式,以及复制和赋值对于类型是否有意义。