Search code examples
c++inheritancemultiple-inheritancemove-semantics

Move assignment operator and virtual inheritance


Similar questions like mine have already been discussed in this community (there are several posts, like this, this, this, this and this), but the most interesting one (for what I would like to discuss here) is this, though it does not really solve my problem. What I would like to discuss is the following warning:

warning: defaulted move assignment for ‘UG’ calls a non-trivial move assignment operator for virtual base ‘G’.

In the last post mentioned, one user answered that this warning is saying that the base class can be moved twice and so

The second move assignment is from an already moved from object, which could cause the contents from the first move assignment to be overwritten.

I understand that this is a problem and it has better be avoided. Now, I have several classes inheriting from a purely virtual base class. Multiple inheritance is also involved, and is represented in the MWE below. What I would like to have is the possibility of using the move constructor and the move assignment operator whenever needed, so that I can do

T t3;
T t2 = std::move(t1);
t3 = std::move(t2);

without worrying about memory leaks, and everything being moved correctly. Presently, T t2 = std::move(t1); works fine, but t3 = std::move(t2); does not. I made a MWE, which represents very well my actual code, and I'm quite convinced that a solution for the MWE will also be a solution for my code. The MWE is:

class G {
public:
    G() = default;
    G(G&&) = default;
    G(const G&) = default;
    virtual ~G() = default;
    G& operator= (G&& g) {
        cout << __PRETTY_FUNCTION__ << endl;
        return *this;
    }
    G& operator= (const G&) = default;
    virtual void asdf() = 0; // abstract function to force complexity
    string mem_G;
};
class UG : virtual public G {
public:
    UG() = default;
    UG(UG&& u) = default;
    UG(const UG&) = default;
    virtual ~UG() = default;
    UG& operator= (UG&&) = default;
    UG& operator= (const UG&) = default;
    void asdf() { mem_G = "asdf"; }
    string mem_UG;
};
class T : virtual public G {
public:
    T() = default;
    T(T&& t) = default;
    T(const T&) = default;
    virtual ~T() = default;
    T& operator= (T&&) = default;
    T& operator= (const T&) = default;
    virtual void qwer() = 0;
    string mem_T;
};
class FT : public UG, virtual public T {
public:
    FT() = default;
    FT(FT&& f) = default;
    FT(const FT&) = default;
    virtual ~FT() = default;
    FT& operator= (FT&&) = default;
    FT& operator= (const FT&) = default;
    friend ostream& operator<< (ostream& os, const FT& r) {
        os << "    mem_G: " << r.mem_G << endl;
        os << "    mem_UG: " << r.mem_UG << endl;
        os << "    mem_T: " << r.mem_T << endl;
        os << "    mem_FT: " << r.mem_FT;
        return os;
    }
    void qwer() { mem_FT = "zxvc"; }
    string mem_FT;
};

Using the classes in the example, the function

void test() {
    FT c1;
    c1.mem_G = "I am G";
    c1.mem_UG = "I am UG";
    c1.mem_T = "I am T";
    c1.mem_FT = "I am FT";
    cout << "c1" << endl;
    cout << c1 << endl;

    cout << "Move constructor" << endl;
    FT c2 = std::move(c1);
    cout << "c1" << endl;
    cout << c1 << endl;
    cout << "c2" << endl;
    cout << c2 << endl;

    cout << "Move assignment operator" << endl;
    c1 = std::move(c2);
    cout << "c1" << endl;
    cout << c1 << endl;
    cout << "c2" << endl;
    cout << c2 << endl;
}

produces the output (without the comments, which I added for a better understanding of the output)

c1
    mem_G: I am G
    mem_UG: I am UG
    mem_T: I am T
    mem_FT: I am FT
Move constructor      // correct move of 'c1' into 'c2'
c1
    mem_G: 
    mem_UG: 
    mem_T: 
    mem_FT: 
c2
    mem_G: I am G
    mem_UG: I am UG
    mem_T: I am T
    mem_FT: I am FT
Move assignment operator  // moving 'c2' into 'c1' using the move operator will move G's memory twice
G& G::operator=(G&&)      // moving once ...
G& G::operator=(G&&)      // moving twice ... (not really, because that is not implemented!)
c1
    mem_G: 
    mem_UG: I am UG
    mem_T: I am T
    mem_FT: I am FT
c2
    mem_G: I am G         // this memory hasn't been moved because G::operator(G&&)
    mem_UG:               // does not implement the move.
    mem_T: 
    mem_FT:

Notice how mem_G in its last appearance kept its value in c2. In case I defaulted G& operator=(G&&) instead of defining it, the result only differs in that line:

c2
    mem_G:                // this memory has been moved twice

Question How do I implement the move assignment operators (and the move constructors, in case it is needed) within this inheritance structure so that both move the memory only once? Is it possible to have such code without the warning above?

Thanks in advance.


Edit This problem has been solved thanks to this answer. I thought people would find it useful to see a complete proposal of a solution, so I'm adding an extended version of the MWE with two more classes so that it is a little bit more complicated. Also, there is the main function so the classes can be tested. Finally, I would like to add that valgrind does not complain about memory leaks when executing a debug compilation of the code.

Edit I completed the example following the rule of 5, just like one of the users who commented on this answer pointed out, and I thought I would update the answer. The code compiles with no warning with the flags -Wall -Wpedantic -Wshadow -Wextra -Wconversion -Wold-style-cast -Wrestrict -Wduplicated-cond -Wnon-virtual-dtor -Woverloaded-virtual and the execution with valgrind does not produce any errors. I've also added couts with the __PRETTY_FUNCTION__ macro so that anyone who wishes to test the code can see the trace of function calls.

#include <functional>
#include <iostream>
#include <string>
using namespace std;
class G {
public:
    G() {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_G = "empty";
    }
    G(const G& g) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_G(g);
    }
    G(G&& g) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_G(std::move(static_cast<G&>(g)));
    }
    virtual ~G() { }
    G& operator= (const G& g) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_G(g);
        return *this;
    }
    G& operator= (G&& g) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_G(std::move(static_cast<G&>(g)));
        return *this;
    }
    friend ostream& operator<< (ostream& os, const G& r) {
        os << "    mem_G: " << r.mem_G;
        return os;
    }
    virtual void asdf() = 0;
    string mem_G;
protected:
    void copy_full_G(const G& g) {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_G = g.mem_G;
    }
    void move_full_G(G&& g) {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_G = std::move(g.mem_G);
    }
};
class UG : virtual public G {
public:
    UG() : G() {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_UG = "empty";
    }
    UG(const UG& u) : G() {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_UG(u);
    }
    UG(UG&& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_UG(std::move(static_cast<UG&>(u)));
    }
    virtual ~UG() { }
    UG& operator= (const UG& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_UG(u);
        return *this;
    }
    UG& operator= (UG&& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_UG(std::move(static_cast<UG&>(u)));
        return *this;
    }
    friend ostream& operator<< (ostream& os, const UG& r) {
        os << "    mem_G: " << r.mem_G << endl;
        os << "    mem_UG: " << r.mem_UG;
        return os;
    }
    void asdf() { mem_G = "asdf"; }
    string mem_UG;
protected:
    void copy_full_UG(const UG& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_G(u);
        mem_UG = u.mem_UG;
    }
    void move_full_UG(UG&& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        // move parent class
        move_full_G(std::move(static_cast<G&>(u)));
        // move this class' members
        mem_UG = std::move(u.mem_UG);
    }
};
class DG : virtual public G {
public:
    DG() : G() {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_DG = "empty";
    }
    DG(const DG& u) : G() {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_DG(u);
    }
    DG(DG&& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_DG(std::move(static_cast<DG&>(u)));
    }
    virtual ~DG() { }
    DG& operator= (const DG& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_DG(u);
        return *this;
    }
    DG& operator= (DG&& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_DG(std::move(static_cast<DG&>(u)));
        return *this;
    }
    friend ostream& operator<< (ostream& os, const DG& r) {
        os << "    mem_G: " << r.mem_G << endl;
        os << "    mem_DG: " << r.mem_DG;
        return os;
    }
    void asdf() { mem_G = "asdf"; }
    string mem_DG;
protected:
    void copy_full_DG(const DG& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_G(u);
        mem_DG = u.mem_DG;
    }
    void move_full_DG(DG&& u) {
        cout << __PRETTY_FUNCTION__ << endl;
        // move parent class
        move_full_G(std::move(static_cast<G&>(u)));
        // move this class' members
        mem_DG = std::move(u.mem_DG);
    }
};
class T : virtual public G {
public:
    T() : G() {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_T = "empty";
    }
    T(const T& t) : G() {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_only_T(t);
    }
    T(T&& t) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_only_T(std::move(static_cast<T&>(t)));
    }
    virtual ~T() { }
    T& operator= (const T& t) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_only_T(t);
        return *this;
    }
    T& operator= (T&& t) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_only_T(std::move(static_cast<T&>(t)));
        return *this;
    }
    friend ostream& operator<< (ostream& os, const T& r) {
        os << "    mem_G: " << r.mem_G << endl;
        os << "    mem_T: " << r.mem_T;
        return os;
    }
    virtual void qwer() = 0;
    string mem_T;
protected:
    // Copy *only* T members.
    void copy_only_T(const T& t) {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_T = t.mem_T;
    }
    // Move *only* T members.
    void move_only_T(T&& t) {
        cout << __PRETTY_FUNCTION__ << endl;
        // if we moved G's members too then we
        // would be moving G's members twice!
        //move_full_G(std::move(static_cast<G&>(t)));
        mem_T = std::move(t.mem_T);
    }
};
class FT : public UG, virtual public T {
public:
    FT() : T(), UG(){
        cout << __PRETTY_FUNCTION__ << endl;
        mem_FT = "empty";
    }
    FT(const FT& f) : G(), T(), UG() {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_FT(f);
    }
    FT(FT&& f) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_FT(std::move(static_cast<FT&>(f)));
    }
    virtual ~FT() { }
    FT& operator= (const FT& f) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_FT(f);
        return *this;
    }
    FT& operator= (FT&& other) {
        cout << __PRETTY_FUNCTION__ << endl;
        // Move-assign FT members
        move_full_FT(std::move(static_cast<FT&>(other)));
        return *this;
    }
    friend ostream& operator<< (ostream& os, const FT& r) {
        os << "    mem_G: " << r.mem_G << endl;
        os << "    mem_UG: " << r.mem_UG << endl;
        os << "    mem_T: " << r.mem_T << endl;
        os << "    mem_FT: " << r.mem_FT;
        return os;
    }
    void qwer() { mem_FT = "zxvc"; }
    string mem_FT;
protected:
    void copy_full_FT(const FT& f) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_UG(f);
        copy_only_T(f);
        mem_FT = f.mem_FT;
    }
    void move_full_FT(FT&& other) {
        cout << __PRETTY_FUNCTION__ << endl;
        // Move-assign UG members and also the base class's members
        move_full_UG(std::move(static_cast<UG&>(other)));
        // Move-assign only T's members
        move_only_T(std::move(static_cast<T&>(other)));
        // move this class' members
        mem_FT = std::move(other.mem_FT);
    }
};
class RT : public DG, virtual public T {
public:
    RT() : T(), DG() {
        cout << __PRETTY_FUNCTION__ << endl;
        mem_RT = "empty";
    }
    RT(const RT& f) : G(), T(), DG() {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_RT(f);
    }
    RT(RT&& r) {
        cout << __PRETTY_FUNCTION__ << endl;
        move_full_RT(std::move(static_cast<RT&>(r)));
    }
    virtual ~RT() { }
    RT& operator= (const RT& r) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_RT(r);
        return *this;
    }
    RT& operator= (RT&& r) {
        cout << __PRETTY_FUNCTION__ << endl;
        // Move-assign RT members
        move_full_RT(std::move(static_cast<RT&>(r)));
        return *this;
    }
    friend ostream& operator<< (ostream& os, const RT& r) {
        os << "    mem_G: " << r.mem_G << endl;
        os << "    mem_DG: " << r.mem_DG << endl;
        os << "    mem_T: " << r.mem_T << endl;
        os << "    mem_RT: " << r.mem_RT;
        return os;
    }
    void qwer() { mem_RT = "zxvc"; }
    string mem_RT;
protected:
    void copy_full_RT(const RT& f) {
        cout << __PRETTY_FUNCTION__ << endl;
        copy_full_DG(f);
        copy_only_T(f);
        mem_RT = f.mem_RT;
    }
    void move_full_RT(RT&& other) {
        cout << __PRETTY_FUNCTION__ << endl;
        // Move-assign DG members and also the base class's members
        move_full_DG(std::move(static_cast<DG&>(other)));
        // Move-assign only T's members
        move_only_T(std::move(static_cast<T&>(other)));
        // move this class' members
        mem_RT = std::move(other.mem_RT);
    }
};
template<class C> void test_move(const function<void (C&)>& init_C) {
    C c1;
    cout << c1 << endl;
    init_C(c1);
    cout << "Initialise c1" << endl;
    cout << c1 << endl;
    cout << "Move constructor: 'c2 <- c1'" << endl;
    C c2 = std::move(c1);
    cout << "c1" << endl;
    cout << c1 << endl;
    cout << "c2" << endl;
    cout << c2 << endl;
    cout << "Move assignment operator: 'c1 <- c2'" << endl;
    c1 = std::move(c2);
    cout << "c1" << endl;
    cout << c1 << endl;
    cout << "c2" << endl;
    cout << c2 << endl;
}
template<class C> void test_copy(const function<void (C&)>& init_C) {
    C c1;
    cout << c1 << endl;
    cout << "Initialise c1" << endl;
    init_C(c1);
    cout << c1 << endl;
    cout << "Copy constructor: 'c2 <- c1'" << endl;
    C c2 = c1;
    cout << "c1" << endl;
    cout << c1 << endl;
    cout << "c2" << endl;
    cout << c2 << endl;
    cout << "Copy assignment operator: 'c1 <- c2'" << endl;
    c1 = c2;
    cout << "c1" << endl;
    cout << c1 << endl;
    cout << "c2" << endl;
    cout << c2 << endl;
}
template<class C>
void test(const string& what, const function<void (C&)>& init_C) {
    cout << "********" << endl;
    cout << "** " << what << " **" << endl;
    cout << "********" << endl;
    cout << "----------" << endl;
    cout << "-- MOVE --" << endl;
    cout << "----------" << endl;
    test_move<C>(init_C);
    cout << "----------" << endl;
    cout << "-- COPY --" << endl;
    cout << "----------" << endl;
    test_copy<C>(init_C);
}
int main() {
    test<UG>(
    "UG",
    [](UG& u) -> void {
        u.mem_G = "I am G";
        u.mem_UG = "I am UG";
    }
    );
    test<DG>(
    "DG",
    [](DG& d) -> void {
        d.mem_G = "I am G";
        d.mem_DG = "I am DG";
    }
    );
    test<FT>(
    "FT",
    [](FT& u) -> void {
        u.mem_G = "I am G";
        u.mem_UG = "I am UG";
        u.mem_T = "I am T";
        u.mem_FT = "I am FT";
    }
    );
    test<RT>(
    "RT",
    [](RT& u) -> void {
        u.mem_G = "I am G";
        u.mem_DG = "I am DG";
        u.mem_T = "I am T";
        u.mem_RT = "I am RT";
    }
    );
}

Solution

  • The problem is that FT's FT& operator= (FT&&) = default; is essentially:

    FT& operator=(FT&& other) {
        // Move-assign base classes
        static_cast<UG&>(*this) = std::move(static_cast<UG&>(other));  // Also move-assigns G
        // other.mem_G is now empty after being moved
        static_cast<T&>(*this) = std::move(static_cast<T&>(other));  // Also move-assigns G
        // this->mem_G is now empty
        // Move-assign members
        mem_FT = std::move(other.mem_FT);
    }
    

    (Though not exactly. A compiler is allowed to be smart and only move from a virtual base class once, but that doesn't happen with gcc and clang at least)

    Where the single base class subobject G is moved into from other twice (through the two move assigns). But other.mem_G is empty after the first move, so it will be empty after the move assign.

    The way to deal with this is to make sure that the virtual base is only move-assigned once. This can easily be done by writing something like this:

    FT& operator=(FT&& other) noexcept {
        // Also move-assigns `G`
        static_cast<T&>(*this) = std::move(static_cast<T&>(other));
        // Move-assign UG members without UG's move assign that moves `G`
        mem_UG = std::move(other.mem_UG);
        // Move-assign FT members
        mem_FT = std::move(other.mem_FT);
    }
    

    With private members or a more complicated move-assign, you might want to make a protected move_only_my_members_from_this_type_and_not_virtual_bases(UG&&) member function

    You can also fix this by not generating a default move-assign operator, making the base class be copied twice instead of becoming empty, for a potential performance hit.