Localizing Code With std::variant
As we all know, C++17 is on the horizon. With it’s release will comes a new member to the data container family, std::variant
.
Looking at std::variant
Let’s take a look at it’s definition:
template <class... Types>
class variant;
Variant types are useful for all types of applications, particularly when you want to flatten hierarchies into simpler, more independent types.
Let’s see a simple use case for std::variant
:
#include <iostream>
#include <string>
#include <variant>
int main() {
using namespace std::string_literals;
std::variant<int, std::string> v;
// use std::string side
v = "Hello, World!"s;
std::cout<<std::get<std::string>(v)<<'\n';
// use int side
v = 42;
std::cout<<"Meaning of life: "<<std::get<int>(v)<<'\n';
}
As contrived as this example may be it shows how flexable std::variant
can be. It allows us to bind several types together that can have semantic meaning within our application without the types poisoning each other with interface requirements and let’s us treat the base class, std::variant
in this case, as a value type.
Working with variants
Along with std::variant
comes a very welcomed utility meant for generically handling types within a variant, std::visit
defined as follows:
template <class Visitor, class... Variants>
constexpr common_type visit(Visitor&& vis, Variants&&... vars);
There are a couple of things to notice about std::visit
- We accept a single visitor for multiple variant types and,
- variants don’t need to contain homogeneous types across them.
Because of these properties our visitor provides a good solution to the multi-method problem, pre-canned in the STL!
Let’s see a simple use case for std::visit
:
#include <iostream>
#include <string>
#include <variant>
template <typename... Ts>
void print_variant(const std::variant<Ts...>& v) {
struct print_visitor {
void operator()(int i) const { std::cout<<i<<'\n'; }
void operator()(const std::string& s) const { std::cout<<s<<'\n'; }
};
std::visit(print_visitor{}, v);
}
int main() {
using namespace std::string_literals;
std::variant<int, std::string> v;
// use std::string side
v = "Hello, World!"s;
print_variant(v);
// use int side
v = 42;
print_variant(v);
}
As great as this is, do you see a problem? The problem that I see is that, while a visitor object can be locally declared, if we want to handle generic cases we have two options.
Either collapse all of the visit cases into a single generic lambda:
std::visit([](const auto& e) { std::cout<<e<<'\n'; }, v);
Declare your visitor outside of your function with a templated member function to handle the generic cases (due to §14.5.2.2 in the standard).
Luckily, there is a very easy fix for this, lambda composition.
Localizing with std::variant
What are the goals of variants? One could argue some goals behind variants are to:
- Flatten hierarchies
- Mix value semantics with the complexity of interface based types (ie. have a generic interface that multiple types may use)
- Localize behaviours / algorithms
This last aspect is easy to solve with a small library that is meant to compose lambda objects into a single, operator()
overloaded object. Let’s see what print_variant
looks like after this transformation:
#include <iostream>
#include <string>
#include <variant>
#include <lambda_util>
template <typename... Ts>
void print_variant(const std::variant<Ts...>& v) {
std::visit(
lambda_util::compose(
[](int i) { std::cout<<i<<'\n'; },
[](const std::string& s) { std::cout<<s<<'\n'; },
[](const auto&) { std::cout<<"TODO\n"; }),
v);
}
int main() {
using namespace std::string_literals;
std::variant<int, std::string> v;
// use std::string side
v = "Hello, World!"s;
print_variant(v);
// use int side
v = 42;
print_variant(v);
}
With this transformation the reader has all of the necessary code for the behaviour in the function itself. Maintainers will truly appreciate a compact function like this.
Just to show this composer doesn’t do any extra work outside of template metaprogramming, let’s see one more example:
int main() {
std::variant<int, const char*, double> v;
v = 10.5;
return std::visit(
lambda_util::compose(
[](double) { return 0; },
[](const char*) { return 1; },
[](auto) { return 2; }),
v);
}
The compiler I was using was GCC 7 (snapshot)
with -O2 -std=c++1z
. Here is its output:
main:
xorl %eax, %eax
ret
This anecdote speaks to 3 things, the amout of std::variant
that is constexpr
compatible, the lambda_util::compose
lightweight functionality, and the fine folks on the GNU GCC team and the excellent work they do with their optimizer
For those who want to take a good look at lambda_util::compose
’s implementation, see here!.
Until next time!