Переместить конструктор для полиморфных элементов с обратной ссылкой - PullRequest
0 голосов
/ 28 декабря 2018

Я реализовал шаблон State, который также включает ссылки на предметный класс.

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), конструктор перемещения по умолчанию не достаточно, поскольку перемещенное состояние по-прежнему ссылается на старую тему,В одном случае состояние даже требует другого субъекта, который при перемещении приводит к недопустимой ссылке.

Как я могу реализовать правильную семантику перемещения в этой настройке?В настоящее время я резервирую достаточно места в начале, так что переезд не требуется, но в будущем это может оказаться невозможным.

1 Ответ

0 голосов
/ 28 декабря 2018

Поскольку ссылки не могут повторно связываться в C ++, любой экземпляр класса со ссылочным членом не может быть переназначен должным образом, если объект, назначаемый из ссылок, не совпадает с объектом, назначенным для него.Но так как вы хотите иметь возможность перемещать Subject s, вы должны иметь возможность переназначить этих участников.Здесь есть два варианта: вы можете использовать указатели для членов subject_ и target_ или использовать std::reference_wrapper.

Я лично предпочитаю std::reference_wrapper, потому что с указателями кто-то незнакомый с кодом может подумать, что это может быть nullptr, тогда как reference_wrapper дает понять, что ссылка всегда действительна.Однако, в отличие от указателей, std::reference_wrapper требует, чтобы ссылочный тип был полным типом до C ++ 20, поэтому одного предварительного объявления недостаточно, и вам необходимо поменять определения State и Subject в вас.код для его использования (как вы можете видеть в самом конце этого ответа).

Использование std::reference_wrapper изменяет ваши State классы на что-то вроде этого (заметьте, я также добавил недостающее назначение Subject::state_ в конструкторе 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 должен дополнительно обновлять элемент данных target_ при перемещении этого экземпляра Subject.

Поскольку я предполагаю, что вы не хотите вводить связь, в которой Subject нужно знать обо всех State подклассах, которые имеют дополнительные ссылки на Subject s, как и 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_;
};

Конечно, нам также нужно изменить конструкторы State и StateA, чтобы зарегистрировать правильный обратный вызов:

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