Consider the following code:
#include <iostream>
#include <vector>
#include <fstream>
class Person {
std::string name;
public:
Person (const std::string& n) : name(n) { }
Person() = default;
std::string getName() const { return name; }
void save (std::ostream& os) const { os << name << '\n'; }
void load (std::istream& is) { while (std::getline(is, name) && name.empty()); }
};
struct InLoveState {
Person& lovedOne;
void save (std::ostream& os) const {
os << "InLoveState" << '\n';
os << lovedOne.getName() << '\n';
}
};
struct BeatenByParentsState {
Person &dad, &mom;
void save (std::ostream& os) const {
os << "BeatenByParentsState" << '\n';
os << dad.getName() << '\n';
os << mom.getName() << '\n';
}
};
struct InterviewedByThreePeopleState {
Person &president, &vicePresident, &manager;
void save (std::ostream& os) const {
os << "InterviewedByThreePeopleState" << '\n';
os << president.getName() << '\n';
os << vicePresident.getName() << '\n';
os << manager.getName() << '\n';
}
};
class System {
static std::vector<Person*> everybody;
public:
static void load (std::istream& is) {
for (int i = 0; i < 3; ++i) {
Person* person = new Person;
person->load(is);
everybody.push_back(person);
}
}
};
std::vector<Person*> System::everybody;
int main() {
Person bob("Bob"), mary("Mary"), sam("Sam");
InLoveState love{mary};
BeatenByParentsState beaten{bob, mary};
InterviewedByThreePeopleState interviewed{bob, mary, sam};
std::ofstream outfile("Save.txt");
bob.save(outfile);
mary.save(outfile);
sam.save(outfile);
love.save(outfile);
beaten.save(outfile);
interviewed.save(outfile);
std::ifstream infile("Save.txt");
System::load(infile);
}
How to get System::load
to load the states generically in one single Factory method when the states have varying numbers of Person&
data members? One could define
const std::map<std::string, std::function<void(Person&, std::istream&)>> Factory::simultaneousStatesMap = {
{
"InLoveState", [](Person& person, std::istream& is) {
std::string name;
std::getline(is, name);
Person lovedOne = *System::getPersonFromName(name);
InLoveState* state = new InLoveState(lovedOne);
state->load(is);
state->display();
person.addState(*state);
}
},
{
"BeatenByParentsState", [](Person& person, std::istream& is) {
std::string name;
std::getline(is, name);
Person dad = *System::getPersonFromName(name);
std::getline(is, name);
Person mom = *System::getPersonFromName(name);
BeatenByParentsState* state = new BeatenByParentsState(dad, mom);
state->load(is);
state->display();
person.addState(*state);
}
},
{
"InterviewedByThreePeopleState", [](Person& person, std::istream& is) {
std::string name;
std::getline(is, name);
Person president = *System::getPersonFromName(name);
std::getline(is, name);
Person vicePresident = *System::getPersonFromName(name);
std::getline(is, name);
Person manager = *System::getPersonFromName(name);
InterviewedByThreePeopleState* state = new InterviewedByThreePeopleState(president, vicePresident, manager);
state->load(is);
state->display();
person.addState(*state);
}
}
};
But this is obviously horrible and won't scale well when new state classes are defined. So I defined the following class:
template <typename T, int NumReferences>
class InstantiateWithReferences {
std::vector<std::string> allNames;
public:
T* operator()(Person& person, std::istream& is) {
std::string name;
for (int i = 0; i < NumReferences; ++i) {
while (std::getline(is, name) && name.empty());
allNames.push_back(name);
}
return [this] <std::size_t... Is>(std::index_sequence<Is...>&&) {
return new T(person, *System::getPersonFromName(allNames[Is])...);
}(std::make_index_sequence<NumReferences>{});
}
};
This will be used to instantiate the states with their compile-time known number of Person& data members. Then Factory's registerStateFunction
, invoked automatically by static members of the state classes, will be defined by:
static void registerStateFunction (const std::string& type) {
simultaneousStatesMap[type] = [](Person& person, std::istream& is) {
State* state = InstantiateWithReferences<State, State::num_references>()(person, is);
state->load(is);
state->display();
person.addState(*state);
};
}
And now all the states are instantiated generically. Here is my entire solution:
#include <iostream>
#include <vector>
#include <fstream>
#include <functional>
#include <map>
#include <utility>
class PersonState;
class Person {
std::string name;
std::vector<PersonState*> simultaneousStates;
public:
Person (const std::string& n) : name(n) { }
Person() = default;
std::string getName() const { return name; }
void addState (PersonState& state) { simultaneousStates.push_back(&state); }
void save (std::ostream& os) const { os << name << '\n'; }
void load (std::istream& is) { while (std::getline(is, name) && name.empty()); }
};
template <typename T, int NumReferences>
class InstantiateWithReferences {
std::vector<std::string> allNames;
public:
T* operator()(Person& person, std::istream& is) {
std::string name;
for (int i = 0; i < NumReferences; ++i) {
while (std::getline(is, name) && name.empty());
allNames.push_back(name);
}
return generate(person, std::make_index_sequence<NumReferences>{});
}
private:
template <std::size_t... Is> T* generate (Person&, std::index_sequence<Is...>&&); // return new T(person, *System::getPersonFromName(allNames[Is])...);
};
struct Factory {
static std::map<std::string, std::function<void(Person&, std::istream&)>> simultaneousStatesMap;
template <typename State>
static void registerStateFunction (const std::string& type) {
simultaneousStatesMap[type] = [](Person& person, std::istream& is) {
State* state = InstantiateWithReferences<State, State::num_references>()(person, is);
state->load(is);
state->display();
person.addState(*state);
};
}
};
std::map<std::string, std::function<void(Person&, std::istream&)>> Factory::simultaneousStatesMap;
template <typename State>
class StateRegistrar {
public:
StateRegistrar (const std::string& type) { Factory::registerStateFunction<State>(type); }
};
class PersonState {
Person& stateOwner;
protected:
PersonState (Person& person) : stateOwner(person) { }
};
class InLoveState : public PersonState {
Person& lovedOne;
int loveScore;
static const std::string tag;
static const StateRegistrar<InLoveState> registrar;
public:
static constexpr int num_references = 1;
InLoveState (Person& stateOwner, Person& love, int score) : PersonState(stateOwner), lovedOne(love), loveScore(score) { }
InLoveState (Person& stateOwner, Person& love) : PersonState(stateOwner), lovedOne(love) { }
void save (std::ostream& os) const {
os << tag << '\n';
os << lovedOne.getName() << '\n';
os << loveScore << '\n';
}
void load (std::istream& is) { is >> loveScore; }
void display() const { std::cout << "lovedOne = " << lovedOne.getName() << ", loveScore = " << loveScore << '\n'; }
};
const std::string InLoveState::tag = "InLoveState";
const StateRegistrar<InLoveState> InLoveState::registrar(tag);
class BeatenByParentsState : public PersonState {
Person &dad, &mom;
int numBruises, numBeatings;
static const std::string tag;
static const StateRegistrar<BeatenByParentsState> registrar;
public:
static constexpr int num_references = 2;
BeatenByParentsState (Person& stateOwner, Person& d, Person& m) : PersonState(stateOwner), dad(d), mom(m) { }
BeatenByParentsState (Person& stateOwner, Person& d, Person& m, int a, int b) : PersonState(stateOwner), dad(d), mom(m), numBruises(a), numBeatings(b) { }
void save (std::ostream& os) const {
os << tag << '\n';
os << dad.getName() << '\n';
os << mom.getName() << '\n';
os << numBruises << ' ' << numBeatings << '\n';
}
void load (std::istream& is) {
is >> std::skipws >> numBruises >> numBeatings;
}
void display() const { std::cout << "dad = " << dad.getName() << ", mom = " << mom.getName() << ", numBruises = " << numBruises << ", numBeatings = " << numBeatings << '\n'; }
};
const std::string BeatenByParentsState::tag = "BeatenByParentsState";
const StateRegistrar<BeatenByParentsState> BeatenByParentsState::registrar(tag);
class InterviewedByThreePeopleState : public PersonState {
Person &president, &vicePresident, &manager;
static const std::string tag;
static const StateRegistrar<InterviewedByThreePeopleState> registrar;
public:
static constexpr int num_references = 3;
InterviewedByThreePeopleState (Person& stateOwner, Person& p, Person& v, Person& m) : PersonState(stateOwner), president(p), vicePresident(v), manager(m) { }
void save (std::ostream& os) const {
os << tag << '\n';
os << president.getName() << '\n';
os << vicePresident.getName() << '\n';
os << manager.getName() << '\n';
}
void load (std::istream&) { }
void display() const { std::cout << "president = " << president.getName() << ", vicePresident = " << vicePresident.getName() << ", manager = " << manager.getName() << '\n'; }
};
const std::string InterviewedByThreePeopleState::tag = "InterviewedByThreePeopleState";
const StateRegistrar<InterviewedByThreePeopleState> InterviewedByThreePeopleState::registrar(tag);
class System {
static std::vector<Person*> everybody;
public:
static void load (std::istream&);
static Person* getPersonFromName (const std::string& name) {
for (Person* person : everybody)
if (person->getName() == name)
return person;
return nullptr;
}
};
std::vector<Person*> System::everybody;
template <typename T, int NumReferences>
template <std::size_t... Is>
T* InstantiateWithReferences<T, NumReferences>::generate (Person& person, std::index_sequence<Is...>&&) {
return new T(person, *System::getPersonFromName(allNames[Is])...);
}
void System::load (std::istream& is) {
for (int i = 0; i < 4; ++i) { // Simplified for this example.
Person* person = new Person;
person->load(is);
everybody.push_back(person);
}
std::string name;
// Will load the states for everybody.back() only, for brevity.
for (int i = 0; i < 3; ++i) { // i < everybody.back()->numStates()
while (std::getline(is, name) && name.empty());
Factory::simultaneousStatesMap.at(name)(*everybody.back(), is);
}
}
int main() {
Person bob("Bob"), mary("Mary"), sam("Sam"), busyPerson("Mr. Busy");
InLoveState love(busyPerson, mary, 10);
BeatenByParentsState beaten(busyPerson, bob, mary, 25, 6);
InterviewedByThreePeopleState interviewed(busyPerson, bob, mary, sam);
busyPerson.addState(love);
busyPerson.addState(beaten);
busyPerson.addState(interviewed);
std::ofstream outfile("Save.txt");
bob.save(outfile);
mary.save(outfile);
sam.save(outfile);
busyPerson.save(outfile);
love.save(outfile);
beaten.save(outfile);
interviewed.save(outfile);
outfile.close();
std::ifstream infile("Save.txt");
System::load(infile);
}
Output:
lovedOne = Mary, loveScore = 10
dad = Bob, mom = Mary, numBruises = 25, numBeatings = 6
president = Bob, vicePresident = Mary, manager = Sam
Update:
What if InterviewedByThreePeopleState
's president
data member is of type President&
instead of Person&
? Then define the member type reference_types
for each state class and do away with num_references
, which won't be general enough anymore. For example,
class InterviewedByThreePeopleState : public PersonState {
President &president;
Person &vicePresident, &manager;
static const std::string tag;
static const StateRegistrar<InterviewedByThreePeopleState> registrar;
public:
using reference_types = std::tuple<President, Person, Person>;
InterviewedByThreePeopleState (Person& stateOwner, President& p, Person& v, Person& m) :
PersonState(stateOwner), president(p), vicePresident(v), manager(m) { }
...
};
Then we instead have:
template <typename T, int NumReferences>
template <std::size_t... Is>
T* InstantiateWithReferences<T, NumReferences>::generate (Person& person, std::index_sequence<Is...>&&) {
return new T(person,
static_cast<std::tuple_element<Is, typename T::reference_types>::type&>
(*System::getPersonFromName(allNames[Is]))...);
}
and State::num_references
in Factory::registerStateFunction
is replaced with std::tuple_size<typename State::reference_types>::value
. As for state classes that do not have any extra reference data members, to avoid having to define
using reference_types = std::tuple<>;
for these state classes, which would be annoying and easy to forget, we instead define
template <typename T>
concept has_reference_types = requires(T) {
typename T::reference_types;
};
and Factory::registerStateFunction
will instead have the lines:
State* state;
if constexpr (has_num_references<State>)
state = InstantiateWithReferences<State, State::num_references>()(person, is);
else if constexpr (has_reference_types<State>)
state = InstantiateWithReferences<State, std::tuple_size<typename State::reference_types>::value>()(person, is);
else
state = new State(person);
where
template <typename T>
concept has_num_references = requires(T) {
T::num_references;
};
to allow my original solution to work if a state class has extra reference data members that are all of type Person&
(to avoid static casting to Person&
for no reason), i.e.
template <typename T, int NumReferences>
template <std::size_t... Is>
T* InstantiateWithReferences<T, NumReferences>::generate (Person& person, std::index_sequence<Is...>&&) {
if constexpr (has_reference_types<T>)
return new T(person, static_cast<std::tuple_element<Is, typename T::reference_types>::type&>(*System::getPersonFromName(allNames[Is]))...);
else
return new T(person, *System::getPersonFromName(allNames[Is])...);
}
All this was tested to work.