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

前端 未结 2 1639
旧巷少年郎
旧巷少年郎 2021-02-06 15:51

I\'m catching a warning under Clang when testing a library under C++11. I\'ve never come across the warning before and searching is not providing too much in the way of reading

相关标签:
2条回答
  • 2021-02-06 16:33

    The standard allows implementations to choose a simple but sometimes broken way to handle memberwise assignment in the presence of virtual bases.

    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.

    This is particularly nasty for move assignment, since it may mean assigning from an already moved-from member.

    0 讨论(0)
  • 2021-02-06 16:36

    The warning seems self-explanatory to me, it's telling you that move-assigning the derived type will result in move-assigning the base twice.

    Reducing it is trivial, just create an inheritance hierarchy using a virtual base and two paths to get to it:

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

    This will print "moved" twice, because the implicit move assignment operators for each of A, B and C will do a memberwise assignment, which means that both A::operator=(A&&) and B::operator=(B&&) will assign the base class. As Alan says, this is a valid implementation of the standard. (The standard specifies that on construction only the most-derived type will construct the virtual base, but it doesn't have the same requirement for assignment).

    This isn't specific to move assignment, changing the base class to only support copy assignment and not move assignment will print "copied" twice:

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

    This happens for exactly the same reason, both of A::operator=(A&&) and B::operator=(B&&) will assign the base class. The compiler doesn't warn for this case, because doing copy assignment twice is (probably) just suboptimal, not wrong. For move-assignment it might lose data.

    If your virtual base doesn't actually have any data that needs to be copied or moved, or only has data members that are trivially copyable, then making it only support copying not moving will suppress the warning:

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

    This copy assignment operator will still get called twice, but since it doesn't do anything there's no problem. Doing nothing twice is still nothing.

    (GCC seems a bit smarter than Clang here, it doesn't warn about the virtual base's move assignment operator being called twice if it's trivial, because a trivial move is equivalent to a copy and so is less likely to be a problem).

    If the virtual base does have data that needs to be copied on assignment then making it do a copy not a move might still be a good choice, but it depends what the type is and does. You might need to define copy and move assignment explicitly at every level of the hierarchy. Virtual bases are tricky and hard to use correctly, especially in the face of copying or moving. Treating types with virtual bases as value types that can be copied and moved easily might be a design error.

    The iostreams hierarchy uses virtual bases, but is done carefully and correctly. The iostream types are non-copyable, only movable, and the derived types define move assignment explicitly to ensure the basic_ios<> base class only gets updated once. Specifically, basic_iostream::operator=(basic_iostream&&) only operates on the basic_istream base, not the basic_ostream one. The equivalent for the example above would be:

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

    Iostreams were not copyable at all until C++11 when rvalue references and move semantics made it possible to do with useful semantics. If your class has always been copyable in C++03 it might already have been a questionable design that should have had been non-copyable, or have carefully written copy operations not implicitly-defined ones.

    In short, any time you have virtual bases you need to think very carefully about how construction, assignment and destruction work, and whether copying and assignment even make sense for the type.

    0 讨论(0)
提交回复
热议问题