A Supercute Approach to Mode Toggling
🔗 Motivation
Suppose you have a Cat
that can be in one of two modes: Angel
and Angry
.
For each state, you’d like to have a few actions that your cat can do: .MakeSound()
and .PlayWithToy()>s
.
In Angel
mode, Cat
needs to know which toys are available.
In Angry
mode, Cat
doesn’t care — Cat
is not interested.
However, in Angry
mode Cat
needs to know the number of times its been asked to .MakeSound()
— after the third time, Cat
will start making growls instead of meows.
We could model this situation with a std::variant<Ts...>
(i.e., std::variant<Angel, Angry>
).
However, when we switch modes any Angel
state stored will be cleared out.
If we switch back to Angel
mode, we won’t know what toys are available!
We could have a std::array<std::variant<Ts...>, len(Ts...)>
.
However, then we’d have to keep an index to track which mode is active.
This has a bit of extra overhead too — we have to have std::variant
machinery and the largest footprint of Angel
or Angry
in every element of the array.
We could have a std::tuple<Ts...>
and maintain a runtime index.
If we didn’t want to expose some kind of enum or indexing convention to the user, we’d have to do a bit of extra legwork.
Plus, runtime indexing into a std::tuple<Ts...>
is generally not super cute.
Especially if we want to do a visitor pattern.
We could have a std::tuple<Ts...>
and use a std::variant<Ts...>
to keep track of what’s active.
We could do a visitor pattern here, using std::get<T>
to switch up which tuple element we operate with based on the active type in the variant.
But the overhead of maintaining a dummy std::variant
of actual Ts...
is not super cute.
We should have a std::tuple<Ts...>
and use a std::variant<std::type_identity<Ts>...>
to keep track of what’s active.
Now that is supercute: std::type_identity
is an empty type, so we can use the std::variant
machinery to track active type with negligible overhead.
🔗 Implementation
I’ve implemented this idea in the DisjointVariant
class below: the idea is to provide similar functionality to std::variant
but with state maintained (instead of cleared) for inactive modes.
#include <tuple>
#include <type_traits>
#include <utility>
#include <variant>
template<typename ...Ts>
class DisjointVariant {
// Holds state for each element.
std::tuple<Ts...> disjoint_data;
/// Tracks which type is active.
std::variant<std::type_identity<Ts>...> active_typeid;
public:
/// Forwarding constructor.
template<typename... Args>
DisjointVariant(Args&&... args)
: disjoint_data(std::forward<Args>(args)...)
{}
/// Switch which data element is active.
template<typename T>
void Activate() {
using wrapped_active_type_t = std::type_identity<T>;
active_typeid.template emplace<wrapped_active_type_t>();
}
/// Assign to data element.
template<typename T>
void AssignToElement(T&& val) {
std::get<T>(disjoint_data) = std::forward<T>(val);
}
/// Assign data element and set that element as active.
template<typename T>
void AssignAndActivate(T&& val) {
AssignToElement<T>( std::forward<T>(val) );
Activate<T>();
}
/// Wraps std::visit to execute visitor on active data element.
template<class Visitor>
decltype(auto) Visit(Visitor&& visitor) {
return std::visit(
[this, &visitor]( const auto& typeid_ ){
using wrapped_active_type_t = std::decay_t<decltype(typeid_)>;
using active_type_t = typename wrapped_active_type_t::type;
auto& active_data = std::get<active_type_t>( disjoint_data );
return std::forward<Visitor>(visitor)(active_data);
},
active_typeid
);
}
};
🔗 Example
To implement our Cat
with DisjointVariant
we could do something like…
#include <iostream>
#include <string>
struct Angel {
std::string available_toy;
void MakeSound() const { std::cout << "purr" << '\n'; }
std::string PlayWithToy() const {
if (available_toy == "mouse") return "pounce";
else if (available_toy == "ball") return "bat";
else return "sniff";
}
};
struct Angry {
size_t poke_count{};
void MakeSound() {
if (++poke_count < 3) std::cout << "meow" << '\n';
else std::cout << "grr" << '\n';
}
std::string PlayWithToy() const { return "glare"; }
};
struct Cat {
DisjointVariant<Angel, Angry> mode;
void MakeSound() {
mode.Visit([](auto&& impl){ impl.MakeSound(); });
}
std::string PlayWithToy() {
return mode.Visit([](auto&& impl){ return impl.PlayWithToy(); });
}
};
🔗 Let’s Chat
Thoughts?
I started a twitter thread (right below) so we can chat
new on the blog,
— mmore500 (@mmore500) September 29, 2021
"A Supercute Approach To Mode Toggling"
/) /)
( ^.^ )
C(") (")https://t.co/djMWQo6lSq
Pop on there and drop me a line or make a comment