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

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

本文关键字:移动 分配 虚拟 基类 运算符 默认 警告      更新时间:2023-10-16

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

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

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

我的第二个问题是(如果需要),我如何处理警告有哪些可用的补救选项


以下是一些附加信息:

  • 编译器:Apple LLVM 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上也审查了以下内容,但我不清楚它们在哪里相交:

  • 用C++11,三条规则变成五条规则

Crypto++库也大量使用Curioly 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.

我很抱歉没有减少它。我不知道如何减少它并抓住警告/投诉的本质。

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

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

#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&&)都将分配基类。正如Alan所说,这是标准的有效实施。(该标准规定,在构造时,只有最派生的类型才会构造虚拟基,但它对赋值没有相同的要求)。

这并不适用于移动赋值,将基类更改为仅支持复制赋值而不支持移动赋值将打印"copied"两次:

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

发生这种情况的原因完全相同,A::operator=(A&&)B::operator=(B&&)都将分配基类。编译器不会对这种情况发出警告,因为两次执行副本分配(可能)只是次优,而不是错误的。对于移动分配,它可能会丢失数据。

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

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

这个复制赋值操作符仍然会被调用两次,但由于它什么都不做,所以没有问题。两次不做仍然是徒劳的。

(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;
     }
};

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

简言之,任何时候你有虚拟基地,你都需要非常仔细地思考构建、分配和销毁是如何工作的,以及复制和分配是否对这种类型有意义。

该标准允许实现选择一种简单但有时会中断的方式来处理存在虚拟基的成员分配。

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

与复制赋值一样,未指定通过继承晶格中的多个路径可访问的虚拟基类子对象是否由隐式定义的移动赋值运算符多次赋值。

这对于移动分配来说尤其糟糕,因为这可能意味着从已经移动的成员中进行分配。