【问题标题】:Move constructor for polymorphic members with back reference使用反向引用移动多态成员的构造函数
【发布时间】:2019-05-26 01:15:32
【问题描述】:

我实现了状态模式,它还包括对主题类的引用。

class State {
public:
    virtual void doStuff() = 0;

protected:
    State(Subject& s) : subject_{s} {}

private:
    Subject& subject_;
};

class StateA : public State {
public: 
    StateA(Subject& s, Subject& target) : State(s), target_{t} {}

    void doStuff() override { /* implementation requiring subject_ */ }  

private:
    Subject& target_;
};

class Subject {
public:
    void doStuff() { state_->doStuff(); }
    State* state_;
};

当我想在需要移动语义的容器中使用主题集合(如std::vector)时,默认的移动构造函数是不够的,因为移动状态仍然引用旧主题。在一种情况下,该状态甚至需要另一个主题,当移动该主题时会导致引用无效。

如何在此设置中实现正确的移动语义?目前,我在开始时预留了足够的空间,因此不需要移动,但将来可能不可行。

【问题讨论】:

  • 你的问题不清楚。您提到您正在使用主题集合,并且主题不是您的代码 sn-ps 中的多态类型。那么你指的是你的 State 对象的移动构造函数吗?我可以看到您在 State 中定义了一个 Copy 构造函数(尽管没有 const 引用参数),这将阻止自动生成移动构造函数,编译器甚至不能使用此复制构造函数代替移动构造函数,因为参数是不是常量引用。所以理想情况下,当你使用移动语义时,你应该得到一个编译器错误
  • @MSSrikkanth 我在哪里定义了复制构造函数?我说的是Subject的移动构造函数。
  • 简单。不要使用引用成员。

标签: c++ state move-semantics


【解决方案1】:

由于引用无法在 C++ 中重新绑定,因此具有引用成员的类的任何实例都无法正确重新分配,除非从引用中分配的对象与分配给的对象相同。但是,当您希望能够移动 Subjects 时,您需要能够重新分配这些成员。这里有两个选项:您可以使用subject_target_ 成员的指针,也可以使用std::reference_wrapper

我个人更喜欢std::reference_wrapper,因为不熟悉代码的人可能会认为它可能是nullptr,而reference_wrapper 清楚地表明引用始终有效。但是,与指针相比,std::reference_wrapper 要求引用的类型在 C++20 之前是完整类型,因此仅前向声明是不够的,您需要交换 StateSubject 的定义代码以便使用它(正如您在此答案的最后看到的那样)。

使用std::reference_wrapper 将您的State 类更改为类似这样(注意,我还在State 的构造函数中添加了Subject::state_ 的缺失赋值):

class State {
public:
    State(Subject& s)
     : subject_{std::ref(s)} {
        s.state_ = this;
    }
    State(State const&) = delete;
    State(State&&) = delete;
    State& operator=(State const&) = delete;
    State& operator=(State&&) = delete;
    virtual ~State() = default;

    virtual void doStuff() = 0;

protected:
    Subject& subject() {
      return subject_;
    }

private:
    std::reference_wrapper<Subject> subject_;
};

class StateA : public State {
public: 
    StateA(Subject& s, Subject& target)
      : State(s),
        target_{std::ref(target)} {
    }

    void doStuff() override { /* implementation requiring subject() */ }  

private:
    Subject& target() {
        return target_;
    }

    std::reference_wrapper<Subject> target_;
};

但正如您还指出的那样,当您移动 Subject 时,您需要通知 State 对象 Subject 已移动,以便调整现在悬空的引用 subject_。但是,您不仅需要通知State 类重新分配subject_ 成员,StateA 类还需要在移动Subject 的实例时更新target_ 数据成员。

由于我假设您不想引入一个耦合,其中 Subject 需要知道所有具有对 Subjects 的附加引用的 State 子类,就像 StateA 一样,因此我们需要一个通用的通知机制这样State 的具体(子)类就可以重新分配适当的reference_wrapper 成员。我的想法是让State 注册一个回调,Subject 在移动时调用它。为此,我会使用std::function。这会将 Subject 类更改为如下所示:

class Subject {
public:
    Subject() = default;
    Subject(Subject const&) = delete;
    Subject(Subject&& other)
      : state_{std::move(other.state_)},
        move_callbacks_{std::move(other.move_callbacks_)} {
        for (auto& callback : move_callbacks_) {
            callback(this);
        }
    }
    Subject& operator=(Subject const&) = delete;
    Subject& operator=(Subject&& other) {
        state_ = std::move(other.state_);
        move_callbacks_ = std::move(other.move_callbacks_);
        for (auto& callback : move_callbacks_) {
            callback(this);
        }
        return *this;
    }
    ~Subject() = default;

    void doStuff() { state_->doStuff(); }

    State* state_ = nullptr;
    std::vector<std::function<void(Subject*)>> move_callbacks_;
};

当然我们还需要修改StateStateA构造函数来注册正确的回调:

State::State(Subject& s)
  : subject_{std::ref(s)} {
    s.state_ = this;
    s.move_callbacks_.emplace_back([this](Subject* new_location) { 
        subject_ = std::ref(*new_location); 
    });
}

StateA::StateA(Subject& s, Subject& target)
  : State(s),
    target_{std::ref(target)} {
    target.move_callbacks_.emplace_back([this](Subject* new_location) {
        target_ = std::ref(*new_location);
    });
}

在重新排序所有内容以便编译后,我们最终得到

#include <cassert>
#include <functional>

class State;

class Subject {
public:
    Subject() = default;
    Subject(Subject const&) = delete;
    Subject(Subject&& other)
      : state_{std::move(other.state_)},
        move_callbacks_{std::move(other.move_callbacks_)} {
        for (auto& callback : move_callbacks_) {
            callback(this);
        }
    }
    Subject& operator=(Subject const&) = delete;
    Subject& operator=(Subject&& other) {
        state_ = std::move(other.state_);
        move_callbacks_ = std::move(other.move_callbacks_);
        for (auto& callback : move_callbacks_) {
            callback(this);
        }
        return *this;
    }
    ~Subject() = default;

    void doStuff();

    State* state_ = nullptr;
    std::vector<std::function<void(Subject*)>> move_callbacks_;
};

class State {
public:
    State(Subject& s)
     : subject_{std::ref(s)} {
       s.state_ = this;
       s.move_callbacks_.emplace_back([this](Subject* new_location) { 
           subject_ = std::ref(*new_location); 
       });
    }
    State(State const&) = delete;
    State(State&&) = delete;
    State& operator=(State const&) = delete;
    State& operator=(State&&) = delete;
    virtual ~State() = default;

    virtual void doStuff() = 0;

protected:
    Subject& subject() {
      return subject_;
    }

private:
    std::reference_wrapper<Subject> subject_;
};

class StateA : public State {
public: 
    StateA(Subject& s, Subject& target)
      : State(s),
        target_{std::ref(target)} {
        target.move_callbacks_.emplace_back([this](Subject* new_location) {
            target_ = std::ref(*new_location);
        });
    }

    void doStuff() override { /* implementation requiring subject() */ }  

private:
    Subject& target() {
        return target_;
    }

    std::reference_wrapper<Subject> target_;
};

void Subject::doStuff() {
    assert(state_ && "Can't call `Subject::doStuff` on a `Subject` that"
                     "doesn't have an associated state!");
    state_->doStuff(); 
}

【讨论】:

  • 感谢您将我指向reference_wrapper。它似乎是原始指针的一个很好的替代方案。不幸的是target_ 本身就有一个状态。所以一个move_callback_ 是不够的。我认为,当时没有办法绕过某种观察者模式并存储“移动订阅者”列表。我只是想使用不涉及任何移动操作的不同数据结构,例如std::list。但我将不得不进一步调查。
  • @Albjenow “target_ 本身有一个状态”是什么意思?一个Subject 可以既是一种状态的subject_ 又是另一种状态的target_?如果不是,那么上面的代码可以正常工作,因为StateA 的构造函数调用的State 的构造函数也注册了subject_ 回调。
  • @Albjenow 我已经扩展了我的答案以允许多个回调,现在即使一个Statesubject_ 是另一个target_ 也可以工作。
猜你喜欢
  • 2013-01-08
  • 2016-01-27
  • 1970-01-01
  • 1970-01-01
  • 2014-11-08
  • 2015-03-07
  • 2013-03-27
  • 2016-12-16
相关资源
最近更新 更多