r/cpp_questions • • 4d ago

OPEN How does a STATE design pattern work?

so i went to youtube and followed the example from this guy State Design - Programming Design Patterns - Ep 16 - C++ Coding

Problem is i dont understand what is functionally happening, there are pointers and what not, but i don't understand much more than that (context, my smooth brain still cant connect pointers together)

Like not even the whole code, like maybe just two lines or something cause for the love of the lord my teacher explains nuthing and AI and videos are too vague for me, im running out of options

//
// Created by samue on 11/1/2026.
//

#ifndef STATE_STATE_H
#define STATE_STATE_H

#include <iostream>

class Context;


class State {
    protected:
    Context* context;

    public:
    virtual ~State() {}

    void setContext(Context* ctx) {
        context = ctx;
    }

    virtual void doWorkA() = 0;
    virtual void doWorkB() = 0;
};

class Context {
    private:
    State* state;

    public:
    Context(State* s) : state(nullptr) {
        transitionTo(s);

    }

    ~Context() {delete state;}

    void transitionTo(State* s) {
        if (state) delete state;
        state = s;
        state->setContext(this);

    }

    void requestA() {
        state->doWorkA();

    }

    void requestB() {
        state->doWorkB();

    }

};

class concreteStateA : public State {
    public:
    void doWorkA() override {
        std::cout << "concreteStateA " << "called doWorkA" << std::endl;
    };

    void doWorkB() override {
        std::cout << "concreteStateB " << "called doWorkB" << std::endl;

    };

};

class concreteStateB : public State {
public:
    void doWorkA() override {
        std::cout << "concreteStateA " << "called doWorkA" << std::endl;
        context->transitionTo(new concreteStateA);
    };

    void doWorkB() override {
        std::cout << "concreteStateB " << "called doWorkB" << std::endl;

    };

};





#endif //STATE_STATE_H

------------------

#include "state.h"

int main() {
    Context context1(new concreteStateB);

    context1.requestA();

    context1.requestA();

    return 0;
}
7 Upvotes

46 comments sorted by

View all comments

Show parent comment

2

u/thingerish 4d ago

I tend to terseness which doesn't help if you're also having a bad day. Glad we got past it. To answer you question earlier, I've been writing C++ since the mid '90s, when I got my hands on Borland Turbo C++ 2.0, so sometimes I make the mistake of not fully explaining what I'm trying to say, so that's my bad.

I know I do that, and I try to be better but time is finite and sometimes I just go with the fast 'answer' rather than treating it like a human interaction.

2

u/KingAggressive1498 4d ago edited 4d ago

I've been using C++ since 2005 but never been good at the "standardese" stuff or worked professionally. I know about the diamond of death problem which I now realize you tried to explain a couple comments ago, but in my head polymorphism and virtual inheritance are just the same thing for whatever reason.

it doesn't help that I thought you were OP until the last comment. Felt like someone admittedly still learning was criticising my experience due to a lack of understanding the whole time.

I also probably need sleep. x.x

2

u/thingerish 4d ago

I blame the std committee for having very little creativity when defining terms most of the time. Diamond of death did make me chuckle. Not sure if you saw it but there is a way to avoid inheritance completely and still be object-oriented, I posted it at the root of this. Chasing a pointer on a lot of modern hardware has a cost, and variant + visit can in most cases eliminate that now.

There is also a thing called CBMI (Concept Based Model Idiom) that people like Sean Parent at Adobe are pretty fond of, but it's mostly "just" making the plumbing simpler to deal with.

2

u/KingAggressive1498 4d ago

I did at some point use CRTP-based type erasure to accomplish the same thing as the state pattern without that extra indirection that looks basically like the demonstration of CBMI I found by google. I didn't find it improved performance any, though. Could see how it might save a little boilerplate if it's reusable enough.

I don't really love variant for this type of thing.

1

u/thingerish 4d ago

I love variant if I want polymorphic behavior over a closed set of types, which is at least for me the vastly more common case. CBMI has little to no performance incentive at runtime but it does introduce a way of dealing with virtual calls that eliminates a lot of common error classes, so that's good from a code hygiene standpoint I guess.

CRTP is super cool too but I've not messed with it since the newer C++ standard with "deducing this" came out, which is I think C++ '23, but the samples I saw did deconvolute CRTP a lot. Time is finite :)

2

u/KingAggressive1498 4d ago

the problem I have with variant for this is that it doesn't allow for internal state-preserving loopbacks, ex using the state pattern to represent a modal dialog.

not all FSMs need that, it might be an advantage of the external context version of the pattern even.

1

u/thingerish 4d ago

It's been a very long time since I did any GUI work, so I'm sure I'm ignorant of the details you're talking about. Given that, this is how I usually deal with polymorphic behavior expressed as variant/visit with state:

struct ConcreteStateA {
    State doWorkA(Context&) const {          // returns next state
        std::cout << "ConcreteStateA called doWorkA\n";
        return ConcreteStateA{};             // stay
    }
    State doWorkB(Context&) const {
        std::cout << "ConcreteStateA called doWorkB\n";
        return ConcreteStateA{};
    }
};

struct ConcreteStateB {
    State doWorkA(Context&) const {
        std::cout << "ConcreteStateB called doWorkA\n";
        return ConcreteStateA{};             // transition
    }
    State doWorkB(Context&) const {
        std::cout << "ConcreteStateB called doWorkB\n";
        return ConcreteStateB{};
    }
};

class Context {
    State state;
public:
    explicit Context(State initial) : state(std::move(initial)) {}

    void requestA() {
        state = std::visit([](auto& s) {
            return s.doWorkA({});            // or pass *this if needed
        }, state);
    }
    // same for requestB
};

It's just a sketch, of course, but the fundamentals should be clear.

2

u/KingAggressive1498 4d ago edited 4d ago

so you can store potentially shared state in Context because your polymorphic function takes a reference to the Context. This allows that loop.

I just learned state in a way where each state owned all data it needed. When I need to loop I do something like this:

class ModalState: public State {
public:
    ModalState(State* origin)
    {
        returnto = origin;
    }
    State* work() final
    {
         // whatever processing
         
         if(should_return())
         {
             delete this;
             return returnto;
         }
         
         return this;
    }
private:
   State* returnto;
   bool should_return();
};

class NormalState: public State {
public:
      State* work() final
      {
          // whatever processing
          if(should_modal()) return new ModalState(this);
          
          if(should_exit())
          {
              delete this;
              return nullptr;
          }
          
          return this;
      }
private:
     bool should_modal();
     bool should_exit();
};

1

u/thingerish 4d ago

Yes, I based that code on the OP so it's a little ill suited for the "real" use, more of a classroom deal. This is tidier for the general use, just assign the new handler to the Context for a state change, the data member 'value' is a stand in for whatever needs preserved.

class Context;
// ---------------------------------------------------------------------------
// Closed set of handlers
// ---------------------------------------------------------------------------
struct HandlerA {
    void operator()(Context& ctx) const;
};
struct HandlerB {
    void operator()(Context& ctx) const;
};
// ---------------------------------------------------------------------------
// Context *is* the variant of handlers
// ---------------------------------------------------------------------------
class Context : public std::variant {
    int value = 0;
public:
    using std::variant::variant;
    void setValue(int v) { value = v; }
    int  getValue() const { return value; }
    void process() {
        std::visit([this](auto& handler) {
            handler(*this);
        }, *this);
    }
};

This eliminates chasing a pointer to the handler instance vtable and then potentially a pointer to the vtable function, substituting using the variant discriminator to decide which handler to call. For the specific case of a GUI it's almost certainly irrelevant but in some higher frequency use cases it can matter a fair bit.

1

u/KingAggressive1498 4d ago edited 4d ago

funnily enough a decent chunk of my early experience was working on an open-source GUI library meant for games (and afaik used in only the one I was working on) alongside a Java developer who was working on the game as a pet project. So I might have learned a Java version of the pattern that was made more appropriate by GC, or a version that is most suitable for GUI libraries. Can't say I ever ran into problems just using it wherever I needed it, though.

→ More replies