C ++ State Pattern实现:指向State Machine的指针机制变为无效?

时间:2016-01-03 01:29:03

标签: c++ pointers c++14 dangling-pointer

在尝试从“Head First Design Patterns”一书中实现一个简单的State Pattern示例时,我遇到了一种让我觉得奇怪的情况。请注意,这个问题不是关于正确实现模式,而是关于理解导致观察到的行为的潜在机制。

机器“Gumball_machine”应该具有几种可能的状态(No_quarter_stateHas_quarter_stateSold_out_state等),在运行时通过虚函数调用委托给哪些行为。这些状态是从抽象基类State公开继承的。 Gumball_machine有一个std::unique_ptr<State>State类本身是指向Gumball_machine的原始指针(因为没有所有权)。

当满足某些条件时发生状态转换,它们通过分配新的具体状态类并将所有权转移到Gumball_machine来实现。

(我将在本文末尾发布一些代码示例,因为我想先“明白”。)

有一种情况,在切换状态后,在同一个函数中,调用另一个函数:

void Has_quarter_state::turn_crank()
{         
     std::cout << "You turned...\n";
     machine_->state_ = std::make_unique<Sold_state>(machine_);
     machine_->dispense();                    // Invalid read!

     // This works however (don't forget to comment out the above reallocation):
//     Gumball_machine* ptr{machine_};
//     machine_->state_ = std::make_unique<Sold_state>(machine_);
//     ptr->dispense();
}

machine_是指向Gumball_machine的指针,state_是具体状态std::unique_ptr<State>的{​​{1}}。

如果我声明临时指针Has_quarter_state并调用ptr,则没有问题。但是,如果我只是调用Gumball_machine::dispense(),则valgrind将显示无效读取(错误消息将在下面显示)。

这个我真的不明白。 machine_->dispense()ptr应引用相同的machine_实例,该实例在程序结束前不应销毁。 Gumball_machine(或者更确切地说是父类“State”)只有一个没有所有权的原始指针。

现在我想起来了,可能是因为Has_quarter_state - reset会导致unique_ptr实例占用的内存被释放。这可能意味着任何后续操作,即对Has_quarter_state的函数调用,都会导致未定义的行为。这个假设是否正确? 如果内存地址(Gumball_machine::dispense())没有变化,为什么在我致电&memory_ == &ptrptr->dispense()时会有所不同?

我觉得有一些复杂的内存管理我仍然不明白。希望你能帮助我清理一切。

下面我将发布重现此代码的代码(“不正确”版本)和valgrind给出的错误消息(使用machine_->dispense()--leak-check=full)。

代码使用clang 3.6.0

通过--leak-kinds=all进行编译

现在为实际代码(大大减少为更多的最小例子):

Gumball_machine.hpp:

clang++ -std=c++14 -stdlib=libc++

Gumball_machine.cpp:

#ifndef CLASS_GUMBALL_MACHINE_HPP_
#define CLASS_GUMBALL_MACHINE_HPP_

#include <memory>

class State;

class Gumball_machine
{
     friend class Has_quarter_state;
     friend class Sold_state;
     public:
          Gumball_machine();
          ~Gumball_machine();

          void turn_crank();

     private:
          void dispense();

     private:
          std::unique_ptr<State> state_;

};
#endif

State.hpp:

#include "Gumball_machine.hpp"

#include "Has_quarter_state.hpp"

Gumball_machine::Gumball_machine() : state_{std::make_unique<Has_quarter_state>(this)} {}
Gumball_machine::~Gumball_machine() {}

void Gumball_machine::turn_crank() { state_->turn_crank(); }
void Gumball_machine::dispense() { state_->dispense(); }

State.cpp:

#ifndef CLASS_STATE_HPP_
#define CLASS_STATE_HPP_

class Gumball_machine;

class State
{
     public:
          explicit State(Gumball_machine* m); 
          virtual ~State();

          virtual void turn_crank() = 0;
          virtual void dispense() = 0;

     protected:
          Gumball_machine* machine_ = nullptr;
};
#endif

Has_quarter_state.hpp:

#include "State.hpp"
State::State(Gumball_machine* m) : machine_{m} {}
State::~State() {}

Has_quarter_state.cpp:

#ifndef ClASS_HAS_QUARTER_STATE_HPP_
#define ClASS_HAS_QUARTER_STATE_HPP_

#include "State.hpp"

class Gumball_machine;

class Has_quarter_state : public State
{
     public:
          explicit Has_quarter_state(Gumball_machine*);
          ~Has_quarter_state() override;

          void turn_crank() override;
          void dispense() override;
};
#endif

Sold_state.hpp:

#include "Has_quarter_state.hpp"

#include <iostream>

#include "Gumball_machine.hpp"
#include "Sold_state.hpp"

Has_quarter_state::Has_quarter_state(Gumball_machine* m) : State{m} {}
Has_quarter_state::~Has_quarter_state() {}

void Has_quarter_state::turn_crank()
{         
     std::cout << "You turned...\n";
     machine_->state_ = std::make_unique<Sold_state>(machine_);
     machine_->dispense();                    // Invalid read!

     // This works however (don't forget to comment out the above reallocation):
//     Gumball_machine* ptr{machine_};
//     machine_->state_ = std::make_unique<Sold_state>(machine_);
//     ptr->dispense();
}
void Has_quarter_state::dispense()
{
     std::cout << "No gumball dispensed\n";
}

Sold_state.cpp:

#ifndef ClASS_SOLD_STATE_HPP_
#define ClASS_SOLD_STATE_HPP_

#include "State.hpp"

class Gumball_machine;

class Sold_state : public State
{
     public:
          explicit Sold_state(Gumball_machine*);
          ~Sold_state() override;

          void turn_crank() override;
          void dispense() override;

};
#endif

编辑: main.cpp

#include "Sold_state.hpp"

#include <iostream>

#include "Gumball_machine.hpp"
#include "Has_quarter_state.hpp"

Sold_state::Sold_state(Gumball_machine* m) : State{m} {}
Sold_state::~Sold_state() {}

void Sold_state::turn_crank()
{         
     std::cout << "Turning twice doesn't give you another gumball\n";
}

void Sold_state::dispense()
{
     std::cout << "A gumball comes rolling out the slot\n";
//          machine_->state_.reset(new No_quarter_state{machine_});
     machine_->state_ = std::make_unique<Has_quarter_state>(machine_);
}

最后是valgrind输出:

     int 
main ()
{
     Gumball_machine machine;
     machine.turn_crank();
     return 0;
}

提前感谢您的帮助!

1 个答案:

答案 0 :(得分:1)

问题在于,当您使用新的Has_quarter_state替换turn_crank时,您调用_machine->state的{​​{1}}实例将被销毁:

std::unique_ptr

您要使用包含其他对象的新 machine_->state_ = std::make_unique<Sold_state>(machine_); 替换machine_->state。这意味着在为新unique_ptr构建新~unique_ptr<State>()之前会调用unique_ptr。但是唯一指针的当前拥有对象是Sold_state实例,它在执行方法中由Has_quarter_state隐式引用。

然后你做了什么?

你做this machine_->dispense()但是this->machine_->dispense()是刚刚被销毁的对象的实例变量(并且你在其上调用了当前的执行方法),所以它的值是不再有效。

machine_分配给临时工作,因为您在销毁之前复制了对象的成员字段的内容。因此,您仍然可以正确访问机器。

不使用machine_并强制每个州管理自己的释放,你会看到出现问题,因为(几乎)等效的代码(这将是一个非常糟糕的设计)将是以下内容:

std::unique_ptr

现在您首先看到void Has_quarter_state::turn_crank() { this->machine_->state_ = new Sold_state(); delete this; this->machine_->dispense(); } ,然后尝试访问属于解除分配对象的字段。