Sumant Tambe (17) [Avatar] Offline
#1
In the summary of chapter 9, it's suggested that inheritance is always inferior to std::variant even when we know the types exactly. I'm not sure that's the case.

The advantages of inheritance is that fundamentally, inheritance allows creation of a common abstraction as opposed to a pure sum of types with std::variant. The common abstraction can contain some common state and more importantly common behavior. That would be awkward with std::variant? Finally, recursive sum types can be easily done using inheritance approach. For example the Composite design pattern. Boost has recursive_wrapper for boost::variant. I don’t know if std::variant has such a thing.

Disadvantages of std::variant is that algebraic types such as List t = empty | t List t can’t be represented with std::variant because it’s a valuetype. Boost recursive_wrapper does some heap allocation I believe. Secondly, Invoking a common operation supported by all cases of a variant requires using a std::visit approach. Covariant return types become awkward and add to boilerplate. Is there a way around it?

Consider example,

#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>

struct Engine {
  virtual void start() {}
};
struct V6Engine : Engine {
  virtual void start() {}
};

struct Automobile {
  virtual Engine & get_engine() = 0;  
};

struct Car : Automobile {
  Engine e;
  virtual Engine & get_engine() { return e; }
};

struct Suv : Automobile {
  V6Engine e;
  virtual V6Engine & get_engine() { return e; }
};

int main(void) 
{
    using Vehicle = std::variant<Car, Suv>;
    using Drive = std::variant<Engine, V6Engine>;
    Vehicle vehicle = Car();

    // boilerplate
    auto drive = std::visit([](auto& v) -> Drive { return v.get_engine(); }, vehicle);
    std::visit([](auto& d) { return d.start(); }, drive);

    // as opposed to
    Car c;
    Automobile &a = c;
    a.get_engine().start();
}


https://wandbox.org/permlink/MiI1A01aNEZOaQMB
Ivan Cukic (99) [Avatar] Offline
#2
I'll have to clarify that.

I was aiming at inheritance-based-implementation-of-sum-types is worse-by-default than the variant-based-implementation-of-sum-types when we need closed sums. The reason for this claim is three-fold:

- variants clearly communicate what can be inside them, where a unique_ptr<base_class> requires you to know what classes inherit from base_class
- dynamic polymorphism has negative performance impact and it is not needed for closed sum types (though, the main performance problem is remedied by introducing the type ids as in the example)
- variants require much less boilerplate for defining the type

Regarding states that have common parts - they can be outside of the variant - combining sums and products. Or, you can create a common base class for the states, but still define a variant of specific sub-classes.
Ivan Cukic (99) [Avatar] Offline
#3
p.s. Yes, unfortunately, std::visit + lambdas do introduce a lot of superfluous code...
Sumant Tambe (17) [Avatar] Offline
#4
Perhaps it boils down to the expression problem. As a priori known operations have direct language-level support in the form of virtual functions. Their expressiveness at call site is hard to beat. Adding new operations however become difficult when changing the original source is not allowed. On the other hand, with std::variant, adding operations is easy but calling common operations is verbose.

I really miss C# extension methods.
Ivan Cukic (99) [Avatar] Offline
#5
You might have convinced me. I'll expand that part a bit to mention pros and cons of both approaches.

Initially, I was planning to cover the expression problem, but decided that it is a overly theoretical for this book.