移动具有反向引用的多态成员的构造函数

Move constructor for polymorphic members with back reference

我实现了状态模式,其中还包括对主题 class 的引用。

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)时,默认的移动构造函数是不够的,因为移动状态仍然引用旧主题。在一个实例中,该州甚至需要另一个主题,当移动该主题时会导致无效引用。

如何在此设置中实现正确的移动语义?目前,我一开始就预留了足够的space,所以不需要搬家,但以后可能不可行。

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

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

使用 std::reference_wrapper 将您的 State classes 更改为类似这样的东西(注意我还在 [=22= 的构造函数中添加了缺少的 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_。但是,不仅需要通知Stateclass重新分配subject_成员,StateAclass还需要更新target_ ] 数据成员,当 Subject 的实例被移动时。

因为我假设您不想引入耦合,其中 Subject 需要了解所有 State subclass 有额外引用 Subject 就像 StateA 因此我们需要一个通用的通知机制,以便 State 的具体(子)class 可以重新分配适当的 reference_wrapper 成员。我的想法是让 State 注册一个 Subject 在移动时调用的回调。为此,我会使用 std::function。这会将 Subject class 更改为如下内容:

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