【问题标题】:Move assignment operator and virtual inheritance移动赋值运算符和虚拟继承
【发布时间】:2020-10-12 21:21:52
【问题描述】:

类似我的问题已经在这个社区讨论过(有几个帖子,如thisthisthisthisthis),但最有趣的一个(对于什么我想在这里讨论)是this,虽然它并不能真正解决我的问题。我想讨论的是以下警告:

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

在最后提到的帖子中,one user 回答说这个警告是说基类可以移动两次,所以

第二个移动分配来自一个已经移动的对象,它 可能导致第一次移动分配的内容是 覆盖。

我知道这是一个问题,最好避免。现在,我有几个类继承自纯虚拟基类。还涉及多重继承,并在下面的 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;
};

使用示例中的类,函数

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

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

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&amp; operator=(G&amp;&amp;) 而不是定义它,则结果仅在该行有所不同:

c2
    mem_G:                // this memory has been moved twice

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

提前致谢。


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

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

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

【问题讨论】:

  • 我坚信五法则绝对适用于您拥有虚拟移动构造函数的情况。最好在这里明确一点。这实际上是警告试图告诉你的内容
  • @SebastianHoffmann 什么是“虚拟移动构造函数”?
  • @aschepler 对不起,我的意思是虚拟移动分配
  • @SebastianHoffmann 这里没有任何虚拟赋值函数,但是,如果有的话,事情会变得更加混乱。
  • 好的,所以我们的想法是实现这些运算符。另一个用户发布了一个答案,其中显示了如何在没有内存泄漏的情况下实际做到这一点,所以我会接受它作为帖子的答案。无论如何,谢谢大家。还有@SebastianHoffmann,感谢您提到五法则;我不知道。

标签: c++ inheritance multiple-inheritance move-semantics


【解决方案1】:

问题在于FTFT&amp; operator= (FT&amp;&amp;) = 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);
}

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

单个基类子对象Gother 移入两次(通过两次移动分配)。但是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);
}

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

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

【讨论】:

  • 谢谢!您的建议适用于 MWE,我没有理由认为它不适用于我的实际代码。我会接受你的答案作为正确答案,但我也会发布一个更全面的 MWE 和完整代码的答案,这样人们就有了完整的指南。我希望我没有过火 :) 再次感谢您花时间回答我的问题。
  • 我想我会编辑帖子,而不是发布答案...
猜你喜欢
  • 2020-12-15
  • 2018-11-24
  • 2021-06-07
  • 2012-02-28
  • 1970-01-01
  • 2010-10-14
  • 2021-08-28
  • 1970-01-01
  • 2018-03-17
相关资源
最近更新 更多