【问题标题】:How to implement move-Constructors for derived classes with private members如何为具有私有成员的派生类实现移动构造函数
【发布时间】:2020-04-06 22:45:21
【问题描述】:

以下三个目标会导致冲突:

  • 我了解到,std::move 什么都不做,但之后 std::move(someDObject) 对象someDObject 可能被canibalized,你不应该访问someDObject
  • CleanCode-SingleResponsibiliPattern:一个类应该负责 仅用于一项功能
  • CodingStyle-DataEncapsulation:一个类不应该公开实现细节,甚至不应该暴露给派生类

想象一个(不可复制构造的)类具有两个不同的特征。 SRB 的原因之一是在Base 中实现,第二个在Derived : public Base 中实现。这两个类都将它们的数据保存在一个可移动的容器中,例如std::list<std::string>,分别称为m_dataDerivedm_dataBase。数据封装原因m_dataBase应该是private:

这导致了如何为派生类实现move-Constructor的问题。要么:

Derived::Derived(Derived &&rhs)
: Base(std::move(rhs))
, m_dataDerived(std::move(rhs.m_dataDerived))
{}

这在语法上违反了规则一,在std::move(rhs) 之后不要访问rhs 但是m_dataDerived 不能被Base 的构造函数canibalized,因为Base 对此一无所知=> 因此m_dataDerived 应该仍然有效。我不喜欢应该

反过来又会导致其他问题:

Derived::Derived(Derived &&rhs)
{
    m_dataBase = std::move(rhs.m_dataBase);
    m_dataDerived = std::move(rhs.m_dataDerived);
}

为此,您需要将 m_dataBase 视为 protected: 破坏 DataEncapsulation 的原因。此外,Base 的每个更改都必须在所有派生的 move-Contructors 中完成,这会导致维护问题。

缺少的是std::move(OnlyBasepart ofrhs)。有没有办法做到这一点?

首选的编译示例在onlineGdb 上(但是使用std::vector 而不是std::list)。

除了下面列出的代码:

#include <iostream>
#include <string>
#include <vector>
#include  <algorithm>


class Base;

class Base {
public:
    Base() {};
    Base(Base &&rhs)  {
        std::cout << "BaseMove_Construktor(" << rhs.m_list.size() << ") --> " ;
        m_list = std::move(rhs.m_list);
       std::cout << m_list.size() <<  std::endl;
    }
    Base(std::initializer_list<std::string> &&p_list) {
        int i=0;
        m_list.resize(p_list.size());
        for(auto it = std::begin(p_list); it != std::end(p_list); ++it) {
            m_list[i++] = *it;
        }
    };
    friend std::ostream &::operator<<(std::ostream & oStream, Base const &rhs);
    friend std::ostream &::operator<<(std::ostream & oStream, Base &&rhs);

   int size() { return m_list.size(); }
private:
    std::vector<std::string> m_list;
};

class Derived : public Base {
public:
    Derived() {};
    Derived(Derived &&rhs) : Base(std::move(rhs)) {
        std::cout << "DerivedMove_Construktor(" << Base::size() << ',' << rhs.m_numbers.size() << ')' << std::endl;
        m_numbers = std::move(rhs.m_numbers);
    }
    Derived(std::initializer_list<std::string> &&p_list, std::initializer_list<double> &&p_numbers) 
    : Base(std::move(p_list))
    {
        int i=0;
        std::cout << "Derived-List_Construktor(" << Base::size() << ',' << p_numbers.size() << ')' << std::endl;
        m_numbers.resize(p_numbers.size());
        for(auto it = std::begin(p_numbers); it != std::end(p_numbers); ++it) {
            m_numbers[i++] = *it;
        }
    };
    friend std::ostream &::operator<<(std::ostream & oStream, Derived const &rhs);

private:
    std::vector<double> m_numbers;
};


std::ostream &operator<<(std::ostream & oStream, Base const &rhs) 
{
    oStream << "{ "; 
    for(auto it = std::begin(rhs.m_list); it != std::end(rhs.m_list); ++it) 
    {
        oStream << '"' << *it << "\", ";
    }
    oStream << '}'; 
}

std::ostream &operator<<(std::ostream & oStream, Base &&rhs) 
{
    oStream << "{m: "; 
    for(auto it = std::begin(rhs.m_list); it != std::end(rhs.m_list); ++it) 
    {
        oStream << '"' << *it << "\", ";
    }
    oStream << '}'; 
}

std::ostream &operator<<(std::ostream & oStream, Derived const &rhs) 
{
    oStream << '{'; 
    for(auto it = std::begin(rhs.m_numbers); it != std::end(rhs.m_numbers); ++it) 
    {
        oStream <<  *it << ", ";
    }
    oStream << (Base const &)rhs << '}'; 
}

int main()
{
    std::cout << "Hello World" << std::endl;

    std::cout << Base({ "tafel", "kreide", "Schwamm" }) << std::endl;

    Base base{ "tafel", "kreide", "Schwamm" };
    std::cout << base << std::endl;

    Derived derived({ "tafel", "kreide", "Schwamm" }, { 0.2342, 8.639 });
    std::cout << derived << std::endl;
    Derived derivedCopy(std::move(derived));
    std::cout << "derived is empty now:   " << derived << std::endl;
    std::cout << "derivedCopy holds data: " << derivedCopy << std::endl;

    return 0;
}

【问题讨论】:

  • 小修正:您可以拨打std::move 任何您想要的。只有当生成的右值引用以某种方式被消耗时,移动的值才会变得无效。
  • 这能回答你的问题吗? Move constructor for derived class
  • 是的,隐藏的知识使变体 1 起作用。然而,这对我来说看起来很残酷。将rhs 移出并在下一行访问rhs.m_dataDerived
  • 感谢 JBL,但这篇文章不涉及数据封装和私有成员。正如我在“缺少的是......”中指出的那样,我想得到建议,最终哪种变体更好,甚至更好的第三种方式。
  • @Xantopp 确实没有,但它向您表明,为了保持封装和思想,您想要的第一个变体是“打破”不使用移动后已移动的东西的规则, 实际上在这种情况下很好。

标签: c++ move move-semantics


【解决方案1】:
Derived::Derived(Derived &&rhs)
: Base(std::move(rhs))
, m_dataDerived(std::move(rhs.m_dataDerived))
{}

此代码是正确且惯用的。如果你愿意,你可以用一种方式来说明只有Base 部分是如何移动的:

Derived::Derived(Derived &&rhs)
: Base(std::move(static_cast<Base&&>(rhs)))
, m_dataDerived(std::move(rhs.m_dataDerived))
{}

我们只移动DerivedBase 子对象。实际上,rhsBase 子对象在调用 Base 移动构造函数后处于有效但(按照惯例)未定义的状态,因此我们最好不要对此做任何假设。但是我们显然没有碰m_dataDerived,所以之后离开它就可以了。

我建议不要编写像上面这样的代码(带有额外的static_cast)。对于初学者来说,std::move 实际上变得毫无意义(但将其排除在外会使代码的可读性更差)。在移动构造函数的上下文中,直接从std::move(rhs) 移动构造基的意图和效果应该非常清楚和惯用。

您的第一条规则(“我了解到,std::move 什么都不做,但在std::move(someDObject) 之后,对象someDObject 可能会被蚕食,您不应该访问someDObject)也不准确:

  1. 对象可能被蚕食,但只能通过可以蚕食的操作。所以在调用std::move 之后访问一个对象并不是必然 坏的(但大概有人把std::move 放在那里是有原因的,假设蚕食你不会出错)。

  2. 可以访问移出的对象。但是你不应该对它做任何对其状态做出任何假设的事情(除了它是有效的,这是最终销毁所必需的)。在标准库术语中,您只能对没有先决条件的对象使用操作。所以你可以reset一个被移动的std::unique_ptr,你可以在一个被移动的std::vector上调用size()等等。

这当然与搬家建设没有太大关系,但了解究竟发生了什么是值得的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-07-09
    • 1970-01-01
    • 2013-03-27
    • 2016-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多