Variants have been around in C++ for a long time and C++17 now has std::variant. We will compare inheritance and std::variant for their ability to model sum-types (a fancy name for tagged unions). We will visit std::visit and discuss how it helps us model the pattern matching idiom. Immutability is one of the core pillars of Functional Programming (FP). C++ now allows you to model deep immutability; we'll see a way to do that using the standard library. We'll explore if `return std::move(*this)` makes any sense in C++. Immutability may be a reason for that.
6. Modeling game states using std::variant
struct NormalScore {
Player p1, p2;
int p1_score, p2_score;
};
struct DeuceScore {
Player p1, p2;
};
struct AdvantageScore {
Player lead, lagging;
};
struct GameCompleteScore {
Player winner, loser;
int loser_score;
};
using GameState = std::variant<NormalScore, DeuceScore,
AdvantageScore, GameCompleteScore>;
10. Passing Two Variants to std::visit
std::ostream & operator << (std::ostream& o, const GameState& game) {
std::visit(overloaded {
[&](const NormalScore& ns, const auto& other) {
o << "NormalScore" << ns.p1 << ns.p2 << ns.p1_score << ns.p2_score;
},
[&](const DeuceScore& gc, const auto& other) {
o << "DeuceScore[" << ds.p1 << "," << ds.p2 << "]";
},
[&](const AdvantageScore& as, const auto& other) {
o << "AdvantageScore[" << as.lead << "," << as.lagging << "]";
},
[&](const GameCompleteScore& gc, const auto& other) {
o << "GameComplete[" << gc.winner << gc.loser << gc.loser_score << "]";
}
}, game, someother_variant);
return o;
}
There can be arbitrary number of arbitrary variant types.
The visitor must cover all cases
11. A Template to Inherit Lambdas
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
explicit overloaded(Ts... ts) : Ts(ts)... {}
};
A User-Defined Deduction Guide
explicit overloaded(Ts... ts) : Ts(ts)... {}
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
12. Next GameState Algorithm (all cases in one place)
GameState next (const GameState& now,
const Player& who_scored)
{
return std::visit(overloaded {
[&](const DeuceScore& ds) -> GameState {
if (ds.p1 == who_scored)
return AdvantageScore{ds.p1, ds.p2};
else
return AdvantageScore{ds.p2, ds.p1};
},
[&](const AdvantageScore& as) -> GameState {
if (as.lead == who_scored)
return GameCompleteScore{as.lead, as.lagging, 40};
else
return DeuceScore{as.lead, as.lagging};
},
[&](const GameCompleteScore &) -> GameState {
throw "Illegal State";
},
[&](const NormalScore& ns) -> GameState {
if (ns.p1 == who_scored) {
switch (ns.p1_score) {
case 0: return NormalScore{ns.p1, ns.p2, 15, ns.p2_score};
case 15: return NormalScore{ns.p1, ns.p2, 30, ns.p2_score};
case 30: if (ns.p2_score < 40)
return NormalScore{ns.p1, ns.p2, 40, ns.p2_score};
else
return DeuceScore{ns.p1, ns.p2};
case 40: return GameCompleteScore{ns.p1, ns.p2, ns.p2_score};
default: throw "Makes no sense!";
}
}
else {
switch (ns.p2_score) {
case 0: return NormalScore{ns.p1, ns.p2, ns.p1_score, 15};
case 15: return NormalScore{ns.p1, ns.p2, ns.p1_score, 30};
case 30: if (ns.p1_score < 40)
return NormalScore{ns.p1, ns.p2, ns.p1_score, 40};
else
return DeuceScore{ns.p1, ns.p2};
case 40: return GameCompleteScore{ns.p2, ns.p1, ns.p1_score};
default: throw "Makes no sense!";
}
}
}
}, now);
}
13. Modeling Alternatives with Inheritance
class GameState {
std::unique_ptr<GameStateImpl> _state;
public:
void next(const Player& who_scored) {}
};
class GameStateImpl {
Player p1, p2;
public:
virtual GameStateImpl * next(
const Player& who_scored) = 0;
virtual ~GameStateImpl(){}
};
class NormalScore : public GameStateImpl {
int p1_score, p2_score;
public:
GameStateImpl * next(const Player&);
};
class DeuceScore : public GameStateImpl {
public:
GameStateImpl * next(const Player&);
};
class AdvantageScore : public GameStateImpl {
int lead;
public:
GameStateImpl * next(const Player&);
};
class GameCompleteScore :public GameStateImpl{
int winner, loser_score;
public:
GameStateImpl * next(const Player&);
};
14. Sharing State is easier with Inheritance
class GameState {
std::unique_ptr<GameStateImpl> _state;
public:
void next(const Player& who_scored) {}
Player& who_is_serving() const;
double fastest_serve_speed() const;
GameState get_last_state() const;
};
class GameStateImpl {
Player p1, p2;
int serving_player;
double speed;
GameState last_state;
public:
virtual GameStateImpl * next(
const Player& who_scored) = 0;
virtual Player& who_is_serving() const;
virtual double fastest_serve_speed() const;
virtual GameState get_last_state() const;
};
15. Sharing Common State is Repetitive with std::variant
Player who_is_serving = std::visit([](auto& s) {
return s.who_is_serving();
}, state);
Player who_is_serving = state.who_is_serving();
ceremony!
struct NormalScore {
Player p1, p2;
int p1_score, p2_score;
int serving_player;
Player & who_is_serving();
};
struct DeuceScore {
Player p1, p2;
int serving_player;
Player & who_is_serving();
};
struct AdvantageScore {
Player lead, lagging;
int serving_player;
Player & who_is_serving();
};
struct GameCompleteScore {
Player winner, loser;
int loser_score;
int serving_player;
Player & who_is_serving();
};
16. How about recursive std::variant?
struct NormalScore {
Player p1, p2;
int p1_score, p2_score;
int serving_player;
Player & who_is_serving();
GameState last_state;
};
struct DeuceScore {
Player p1, p2;
int serving_player;
Player & who_is_serving();
GameState last_state;
};
struct AdvantageScore {
Player lead, lagging;
int serving_player;
Player & who_is_serving();
GameState last_state;
};
struct GameCompleteScore {
Player winner, loser;
int loser_score;
int serving_player;
Player & who_is_serving();
GameState last_state;
};
Not possible unless you use recursive_wrapper and dynamic allocation.
Not in C++17.
Dare I say, it’s not algebraic? It does not compose
std::variant is a container. Not an abstraction.
18. Combine Implementation Inheritance with
std::variant
{
using GameState = std::variant<NormalScore_v2, DeuceScore_v2,
AdvantageScore_v2, GameCompleteScore_v2>;
GameState state = NormalScore_v2 {..};
Player who_is_serving = std::visit([](SharedGameState& s) {
return s.who_is_serving();
}, state);
Player who_is_serving = state.who_is_serving();
}
SharedGameState
who_is_serving()
NormalScore_v2 DeuceScore_v2 AdvantageScore_v2 GameCompleteScore_v2
ceremony!
19. Modeling Alternatives
Inheritance std::variant
Dynamic Allocation No dynamic allocation
Intrusive Non-intrusive
Reference semantics (how will you copy a
vector?)
Value semantics
Algorithm scattered into classes Algorithm in one place
Language supported
Clear errors if pure-virtual is not implemented
Library supported
std::visit spews blood on missing cases
Creates a first-class abstraction It’s just a container
Keeps fluent interfaces Disables fluent interfaces. Repeated std::visit
Supports recursive types (Composite) Must use recursive_wrapper and dynamic
allocation. Not in the C++17 standard.