移动赋值运算符和虚继承

Move assignment operator and virtual inheritance

类似我的问题已经在这个社区讨论过(有几个post,比如, this, this, this and ), but the most interesting one (for what I would like to discuss here) is this,虽然没有真正解决我的问题。我想讨论的是以下警告:

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

在最后提到的 post 中,one user 回答说这个警告是说底座 class 可以移动两次,所以

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

我知道这是个问题,最好避免。现在,我有几个 classes 继承自一个纯虚拟基 class。还涉及多重继承,并在下面的 MWE 中表示。我想要的是在需要时使用移动构造函数和移动赋值运算符的可能性,这样我就可以做到

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

无需担心内存泄漏,所有内容都可以正确移动。目前,T t2 = std::move(t1); 工作正常,但 t3 = std::move(t2); 不工作。我制作了一个 MWE,它很好地代表了我的实际代码,而且我非常相信 MWE 的解决方案也将是我的代码的解决方案。 MWE 是:

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

使用例子中的classes,函数

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

生成输出(没有注释,我添加注释是为了更好地理解输出)

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:

请注意 mem_G 在其最后一次出现时如何保持其在 c2 中的值。如果我默认 G& operator=(G&&) 而不是定义它,结果仅在那一行不同:

c2
    mem_G:                // this memory has been moved twice

问题如何在这个继承结构中实现移动赋值运算符(和移动构造函数,如果需要的话),以便两者都只移动内存一次?是否可以在没有上述警告的情况下使用这样的代码?

提前致谢。


编辑 由于 的回答,这个问题已经解决。我认为人们会发现看到一个完整的解决方案建议很有用,所以我添加了一个扩展版本的 MWE,其中还有两个 classes,这样它会稍微复杂一些。此外,还有 main 函数,因此可以测试 classes。最后,我想补充一点,在执行代码的调试编译时,valgrind 不会抱怨内存泄漏。

编辑 我按照 5 的规则完成了示例,就像评论此答案的一位用户指出的那样,我想我会更新答案。代码编译时没有带有标志 -Wall -Wpedantic -Wshadow -Wextra -Wconversion -Wold-style-cast -Wrestrict -Wduplicated-cond -Wnon-virtual-dtor -Woverloaded-virtual 的警告,并且带有 valgrind 的执行不会产生任何错误。我还在 __PRETTY_FUNCTION__ 宏中添加了 couts,这样任何想测试代码的人都可以看到函数调用的痕迹。

#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";
    }
    );
}

问题是 FTFT& operator= (FT&&) = default; 本质上是:

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

(虽然不完全是。允许编译器变得聪明,并且只从虚拟基 class 移动一次,但至少 gcc 和 clang 不会发生这种情况)

其中单个基础 class 子对象 Gother 移入两次(通过两次移动分配)。但是other.mem_G在第一次移动后是空的,所以在移动分配后它会是空的。

处理这个问题的方法是确保虚拟基地只有move-assigned一次。这可以通过编写如下内容轻松完成:

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

对于私有成员或更复杂的 move-assign,您可能需要制作一个受保护的 move_only_my_members_from_this_type_and_not_virtual_bases(UG&&) 成员函数

您也可以通过不生成默认 move-assign 运算符来解决此问题,使基数 class 被复制两次而不是变为空,这可能会影响性能。