🔗 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 :phone: :phone: :phone:

Pop on there and drop me a line :fishing_pole_and_fish: or make a comment :raising_hand_woman: