Arene Base
Fundamental Utilities For Safety Critical C++
Loading...
Searching...
No Matches
variant: A Discriminated Union Type

arene::base::variant is a backport of std::variant, with some modifications to be in line with the general error handling policies of arene_base.

The public header is

The Bazel target is

//:variant

A Discriminated Union Type

arene::base::variant allows you to store an object which is one of a specified list of types. This is similar to a union, but with stronger type safety: the type of the stored object can always be queried, and attempting to access the stored object via the wrong type is a runtime error rather than undefined behaviour; variants are therefore also known as discriminated unions. Beyond type safety, variant allows for types with non-trivial constructors, destructors and assignment operators to be stored directly. This is in contrast to a plain union which would require the manual invocation of constructors, assignment operators, and destructors to properly manage object lifetimes.

Declaring a Variant

variants are declared with a list of types that represent the possible active alternatives. Note that the types do not need to be unique. However, it is unspecified which alternative of a duplicate type is interacted with when using type-based access APIs, and the visitor pattern will not distinguish between them.

Interacting with a Variant

There are two primary ways to interact with a variant: procedurally, and with the visitor pattern.

Procedural Access

With procedural access, the active alternative is queried, and the appropriate accessor method is used to fetch the active alternative. The active alternative can be queried by index or type, and similarly can be accessed by index or type. It is invalid to access an alternative which is not currently active. Doing so will throw arene::base::bad_variant_access.

A simple example of procedural access for printing the active alternative can be seen below:

/// @brief Prints the value of the variant using the given access method.
void print_variant_value(my_variant const& var, access_by access) {
// The variant can be checked for valueless state.
if (var.valueless_by_exception()) {
std::cout << "The variant is valueless by exception.\n";
return;
}
// The index of the active alternative can be queried.
std::cout << "The active alternative is at index: " << var.index() << '\n';
// The value can be accessed by index.
if (access == access_by::index) {
switch (var.index()) {
case 0:
std::cout << "The value is an int: " << arene::base::get<0>(var) << '\n';
break;
case 1:
std::cout << "The value is a float: " << arene::base::get<1>(var) << '\n';
break;
case 2:
std::cout << "The value is a string_view: " << arene::base::get<2>(var) << '\n';
break;
default:
std::cout << "Unknown type in variant.\n";
break;
}
return;
}
// The value can also be accessed by type.
if (access == access_by::type) {
std::cout << "The value is an int: " << arene::base::get<int>(var) << '\n';
std::cout << "The value is a float: " << arene::base::get<float>(var) << '\n';
std::cout << "The value is a string_view: " << arene::base::get<arene::base::string_view>(var) << '\n';
} else {
std::cout << "Unknown type in variant.\n";
}
}
}
void use_procedurally() {
// A variant can be constructed with any of the alternatives.
my_variant var{42};
// The value can be printed using the function above.
print_variant_value(var, access_by::index);
// prints "The value is an int: 42"
// The value can be changed to another alternative.
var = 3.14F;
print_variant_value(var, access_by::type);
// prints "The value is a float: 3.14"
// The value can also be changed to a string_view.
var = "Hello, Variant!";
print_variant_value(var, access_by::index);
// prints "The value is a string_view: Hello, Variant!"
}

Visitor Pattern

The visitor pattern allows for a more structured way to interact with the variant. A visitor is a callable object that can be invoked with all of the possible alternatives of the variant. The visitor can be a function, a lambda, or any invocable object that can accept the types of the alternatives in the variant. The visitor is invoked with the active alternative, and can perform operations based on the type of the alternative. The return types of the alternative handlers in the visitor can be any type, including void, and this will be the return type of visit. The cref qualification of both the visitor and variant are maintained.

Note
If the return type for visit is not explicitly specified, then the return type from every alternative's handler must be exactly the same type. Alternatively, by invoking visit<RetT>(visitor, variant), the requirement is relaxed to require only that the return type from every alternative's handler is implicitly convertible to RetT.

The printing example above can be rewritten using the visitor pattern as follows:

/// @brief A visitor object that prints the value of the variant. Note that it must handle all alternatives of the
/// variant.
struct print_visitor {
auto operator()(int value) const -> int {
std::cout << "The value is an int: " << value << '\n';
return 0;
}
auto operator()(float value) const -> int {
std::cout << "The value is a float: " << value << '\n';
return 1;
}
auto operator()(arene::base::string_view value) const -> int {
std::cout << "The value is a string_view: " << value << '\n';
return 2;
}
};
void use_visit() {
my_variant var{42};
// prints "The value is an int: 42\n0\n"
std::cout << arene::base::visit(print_visitor{}, var) << '\n';
var = 3.14F;
// prints "The value is a float: 3.14\n1\n"
std::cout << arene::base::visit(print_visitor{}, var) << '\n';
var = "Hello, Variant!";
// prints "The value is a string_view: Hello, Variant!\n2\n"
std::cout << arene::base::visit(print_visitor{}, var) << '\n';
}

The visitor pattern is exactly analogous to overload resolution which has been deferred to runtime. In fact, a free function overload set can be exposed for runtime overload resolution across a constrained type set like so:

auto print(int value) -> int {
std::cout << "The value is an int: " << value << '\n';
return 0;
}
auto print(float value) -> int {
std::cout << "The value is a float: " << value << '\n';
return 1;
}
auto print(arene::base::string_view value) -> int {
std::cout << "The value is a string_view: " << value << '\n';
return 2;
}
auto print(my_variant const& var) -> int {
// The visitor can be constructed from a set of overloaded functions.
return arene::base::visit([](auto const& value) { return print(value); }, var);
}

This pattern is useful for building up a DSL around a constrained type set as a form of dynamic polymorphism with different tradeoffs. Typical inheritance with virtual functions makes adding new type-dependent behavior for existing operations easy: you simply implement a new type which inherits from the base and overload the methods. However adding new operations is difficult, because it involves altering all existing derived classes. In contrast, using a variant and the visitor pattern makes adding new operations easy: you simply implement a new visitor. However adding new types to existing operations is difficult, as it involves altering all existing visitors. Which is more appropriate for a given application will depend on system design.

bind_overloads

The arene::base::bind_overloads utility function, provided by the //:functional subpackage, can be used to simplify the creation of visitors for use with arene::base::variant, allowing the easy use of inline lambdas to create visitors, like so:

void use_bind_overloads() {
my_variant var{"Hello, Variant!"};
// prints "The value is a string_view: Hello, Variant!\n2\n"
std::cout << arene::base::visit(
[](int value) {
std::cout << "The value is an int: " << value << '\n';
return 0;
},
[](float value) {
std::cout << "The value is a float: " << value << '\n';
return 1;
},
std::cout << "The value is a string_view: " << value << '\n';
return 2;
}
),
var
)
<< '\n';
}