llu*_*lpu 5 c++ inheritance multiple-inheritance move-semantics
像我这样的类似问题已经在这个社区中讨论过(有几个帖子,比如this、this、this、this和this),但最有趣的一个(对于我想在这里讨论的内容)是this,尽管它确实并没有真正解决我的问题。我想讨论的是以下警告:
\nwarning: 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 中表示。我想要的是在需要时可以使用移动构造函数和移动赋值运算符,这样我就可以做
\nT 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 为:
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使用示例中的类,函数
\nvoid 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产生输出(没有注释,我添加注释是为了更好地理解输出)
\nc1\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&&)
而不是定义它,结果仅在该行有所不同:
c2\n mem_G: // this memory has been moved twice\n
Run Code Online (Sandbox Code Playgroud)\n问题如何在此继承结构中实现移动赋值运算符(以及移动构造函数,如果需要),以便两者仅移动内存一次?是否可以有这样的代码而不出现上述警告?
\n提前致谢。
\n编辑由于这个答案,这个问题已经解决。我认为人们会发现看到完整的解决方案提案很有用,因此我添加了 MWE 的扩展版本,其中包含另外两个类,因此它稍微复杂一些。此外,还有main
可以测试类的功能。最后,我想补充一点,在执行代码的调试编译时,valgrind 不会抱怨内存泄漏。
编辑我按照 5 规则完成了示例,就像评论此答案的一位用户指出的那样,我想我会更新答案。代码编译时不会出现带有标志的警告-Wall -Wpedantic -Wshadow -Wextra -Wconversion -Wold-style-cast -Wrestrict -Wduplicated-cond -Wnon-virtual-dtor -Woverloaded-virtual
,并且执行时valgrind
不会产生任何错误。我还在宏cout
中添加了 s __PRETTY_FUNCTION__
,以便任何希望测试代码的人都可以看到函数调用的跟踪。
#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
问题是FT
sFT& 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&&)
成员函数
您还可以通过不生成默认的移动分配运算符来解决此问题,使基类被复制两次而不是变空,从而潜在地提高性能。
归档时间: |
|
查看次数: |
1741 次 |
最近记录: |