移动赋值运算符和虚拟继承

llu*_*lpu 5 c++ inheritance multiple-inheritance move-semantics

像我这样的类似问题已经在这个社区中讨论过(有几个帖子,比如thisthisthisthisthis),但最有趣的一个(对于我想在这里讨论的内容)是this,尽管它确实并没有真正解决我的问题。我想讨论的是以下警告:

\n
warning: defaulted move assignment for \xe2\x80\x98UG\xe2\x80\x99 calls a non-trivial move assignment operator for virtual base \xe2\x80\x98G\xe2\x80\x99.\n
Run Code Online (Sandbox Code Playgroud)\n

在上一篇提到的帖子中,一位用户回答说这个警告是说基类可以移动两次,所以

\n
\n

第二个移动分配来自已移动的对象,\n这可能会导致第一个移动分配的内容\n被覆盖。

\n
\n

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

\n
T t3;\nT t2 = std::move(t1);\nt3 = std::move(t2);\n
Run Code Online (Sandbox Code Playgroud)\n

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

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

使用示例中的类,函数

\n
void test() {\n    FT c1;\n    c1.mem_G = "I am G";\n    c1.mem_UG = "I am UG";\n    c1.mem_T = "I am T";\n    c1.mem_FT = "I am FT";\n    cout << "c1" << endl;\n    cout << c1 << endl;\n\n    cout << "Move constructor" << endl;\n    FT c2 = std::move(c1);\n    cout << "c1" << endl;\n    cout << c1 << endl;\n    cout << "c2" << endl;\n    cout << c2 << endl;\n\n    cout << "Move assignment operator" << endl;\n    c1 = std::move(c2);\n    cout << "c1" << endl;\n    cout << c1 << endl;\n    cout << "c2" << endl;\n    cout << c2 << endl;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

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

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

\n
c2\n    mem_G:                // this memory has been moved twice\n
Run Code Online (Sandbox Code Playgroud)\n

问题如何在此继承结构中实现移动赋值运算符(以及移动构造函数,如果需要),以便两者仅移动内存一次?是否可以有这样的代码而不出现上述警告?

\n

提前致谢。

\n
\n

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

\n

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

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

Art*_*yer 3

问题是FTsFT& 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);
}
Run Code Online (Sandbox Code Playgroud)

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

其中单个基类子对象G被移入other两次(通过两次移动分配)。但other.mem_G在第一次移动后为空,因此在移动分配后它将为空。

处理这个问题的方法是确保虚拟基地仅被移动分配一次。这可以通过编写如下内容轻松完成:

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);
}
Run Code Online (Sandbox Code Playgroud)

对于私有成员或更复杂的移动分配,您可能需要创建受保护的move_only_my_members_from_this_type_and_not_virtual_bases(UG&&)成员函数

您还可以通过不生成默认的移动分配运算符来解决此问题,使基类被复制两次而不是变空,从而潜在地提高性能。