While trying to implement a simple State Pattern example from the book "Head First Design Patterns", I came across a situation which strikes me as kind of peculiar. Mind you, this question is not about implementing the pattern correctly, but about understanding the underlying mechanism that leads to the observed behavior.
The Machine "Gumball_machine" is supposed to have several possible states (No_quarter_state
, Has_quarter_state
, Sold_out_state
, etc.), to which behavior is delegated via virtual function calls at runtime. These states are publicly inherited from an abstract base class State
. Gumball_machine
has an std::unique_ptr<State>
, the State
class itself a raw pointer to the Gumball_machine
(since no ownership is assumed).
A state transition occurs when certain conditions are met, they happen via allocating a new concrete state class and transferring ownership to the Gumball_machine
.
(I will post some code examples at the end of this post, since I want to "get to the point" first.)
There is one situation, where in the same function after having switched states, another function is called:
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();
}
with machine_
being a pointer to the Gumball_machine
, and state_
being an std::unique_ptr<State>
to the concrete state, Has_quarter_state
.
If I declare the temporary pointer ptr
and invoke Gumball_machine::dispense()
, there is no problem. However if I simply call machine_->dispense()
, valgrind will show an invalid read (error message will be shown below).
And this I don't really understand. ptr
and machine_
should refer to the same Gumball_machine
instance, which should not be destroyed until the end of the program. Has_quarter_state
(or rather the parent class "State") only has a raw pointer without ownership.
Now that I think of it, it's probably because the unique_ptr
- reset will cause the memory which is occupied by the Has_quarter_state
instance to be freed. That would probably mean that any subsequent action, i.e. the function call to Gumball_machine::dispense()
, would result in undefined behavior. Is this assumption correct?
If the memory address (&memory_ == &ptr
) doesn't change, why does it make a difference if I call ptr->dispense()
or machine_->dispense()
?
I feel there are some intricacies of memory management that I still don't understand. Hopefully, you will be able to help me clear things up.
Below I will post the code to reproduce this(the "incorrect" version) and the error message valgrind gives me (using --leak-check=full
, --leak-kinds=all
).
The code compiles via clang++ -std=c++14 -stdlib=libc++
using clang 3.6.0
Now for the actual code (greatly reduced to be more of a minimal example):
Gumball_machine.hpp:
#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
Gumball_machine.cpp:
#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.hpp:
#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
State.cpp:
#include "State.hpp"
State::State(Gumball_machine* m) : machine_{m} {}
State::~State() {}
Has_quarter_state.hpp:
#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
Has_quarter_state.cpp:
#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.hpp:
#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
Sold_state.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_);
}
EDIT: main.cpp
int
main ()
{
Gumball_machine machine;
machine.turn_crank();
return 0;
}
And finally the valgrind output:
==12085== Memcheck, a memory error detector
==12085== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==12085== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==12085== Command: ./main
==12085==
==12085== Invalid read of size 8
==12085== at 0x401C61: Has_quarter_state::turn_crank() (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085== by 0x401730: Gumball_machine::turn_crank() (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085== by 0x402FF7: main (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085== Address 0x5e47048 is 8 bytes inside a block of size 16 free'd
==12085== at 0x4C2CE10: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12085== by 0x4017B4: operator delete(void*, unsigned long) (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085== by 0x401858: Has_quarter_state::~Has_quarter_state() (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085== by 0x401B27: Has_quarter_state::turn_crank() (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085== by 0x401730: Gumball_machine::turn_crank() (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085== by 0x402FF7: main (in /home/mbw/Documents/Programmieren/CPP/Design_Patterns/Head_First_Design_Patterns/Chapter10_State_pattern/Example1_Revised/example_for_stackoverflow/main)
==12085==
==12085==
==12085== HEAP SUMMARY:
==12085== in use at exit: 0 bytes in 0 blocks
==12085== total heap usage: 3 allocs, 3 frees, 48 bytes allocated
==12085==
==12085== All heap blocks were freed -- no leaks are possible
==12085==
==12085== For counts of detected and suppressed errors, rerun with: -v
==12085== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Thank you in advance for your help!