[Generic access using visitor functions]
[Deriving from std::variant / using concepts]
With use of a std::variant you can store different types from a known set of possible types within the same variable. The access to the current value is still type safe.
How to define a std::variant
#include <variant>
// Simple struct (used within variant)
struct Coord
{
double x = 0.0;
double y = 0.0;
};
// Define a variant type containing one of four types
using MyVariant = std::variant<int, double, std::string, Coord>;
How to set and get values
MyVariant var1, var2, var3, var4;
// To set a variant's value simply assign a value of defined type
var1 = 4711;
var2 = 15.08;
var3 = "Some text";
var4 = Coord{ 3.14, 2.5 };
// assuming you know the type contained within variant, you can access the contents:
auto dblVal = std::get<double>(var2);
auto coordVal = std::get<Coord>(var4);
// trying to access an illegal type causes an exception
auto illegal = std::get<double>(var1); // throws std::exception, bad variant access
// explicitly checking for the contained type
if (std::holds_alternative<Coord>(var4))
{
auto coord = std::get<Coord>(var4);
}
// explicitly checking for the contained type
if (auto pCoord = std::get_if<Coord>(var4))
{
auto coord = *pCoord;
}
// get current value by index
auto curIdx = var2.index(); // returns 1, double is second listed type
double d = std::get<1>(var2);
Default construction
A default constructed variant has the default value of its first type alternative:
MyVariant var; // contains type int with value 0
If the first type (and perhaps all other types) contained within a variant is (are) not default constructable you can add “std::monostate” as the first type to allow default construction of your variant:
// struct, not default constructable
struct SomeData
{
SomeData(int in_id, const std::string& in_name) : id(in_id), name(in_name){}
int id;
std::string name;
};
using MyVariant1 = std::variant<SomeData, Coord>;
MyVariant1 var; // => compiler error, var is NOT default constructable
// now add "monostate" to the definition
using MyVariant2 = std::variant<std::monostate, SomeData, Coord>;
MyVariant2 var; // all ok, var is now default constructable
Generic access using visitor functions
With use of the visitor pattern you can still use a single function to access type specific information.
// Helper type used for variant visitors
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>;
// Visitor function which extracts numeric value from a variant
double GetNumericValue(const MyVariant & in_variant)
{
auto dblVal = std::visit(overloaded{
[](const int& in_val)-> double {return in_val; },
[](const double & in_val)-> double {return in_val; },
[](const std::string& in_text)-> double {return 0.0; },
/* has no numeric value, return 0.0 */
[](const Coord& in_coord)-> double {return in_coord.x; },
}, in_variant);
return dblVal;
}
// Visitor function for type specific output
void WriteVariant(const MyVariant& in_variant)
{
std::visit(overloaded{
[](const auto& in_val) {std::cout << " " << in_val; },
// called for int, double, std::string
[](const Coord& in_coord) {std::cout << " Coord: "
<< in_coord.x << "/" << in_coord.y << std::endl; },
}, in_variant);
}
Client code:
// Generic access to value via visitor function
double val1 = GetNumericValue(var1);
double val2 = GetNumericValue(var2);
double val3 = GetNumericValue(var3);
double val4 = GetNumericValue(var4);
std::cout << "Numeric values: " << val1 << ", " << val2 << ", "
<< val3 << ", " << val4 << std::endl;
// resulting output: Numeric values: 4711, 15.08, 0, 3.14
// Generic output via visitor function
WriteVariant(var1);
WriteVariant(var2);
WriteVariant(var3);
WriteVariant(var4);
std::cout << std::endl;
// resulting output: 4711 15.08 Some text 3.14/2.5
- function GetNumericValue
Omitting a type which is possible within variant causes an compile error
=> you cannot forget an implementation when variant type is extended
But when one of the variant types is removed you will not get an information from compiler. The functor with the no longer existing variant type will simply not be called. - function WriteVariant
It is possible to summarize all types not listed explicitly under type “auto”
Several alternatives of same type
You can have several logical alternatives within a variant and they can also have the same data type. As a consequence you cannot set/get the value for those entries by using the type. You must work with the index:
using MyVariant = std::variant<int,int>;
// Init value
MyVariant var {std::in_place_index<1>, 42}; // variant gets value 42, used type is second int
auto curIdx = var.index(); // returns 1
// Change value
var.emplace<0>(4711); // variant gets value 4711, used type is first int
curIdx = var.index(); // returns 0
Deriving from std::variant / using concepts
When deriving from std::variant the free accessor functions (see above) become regular class members:
#include <format>
#include <iostream>
#include <string>
#include <variant>
#include <vector>
struct DoubleType { double x{}; };
struct PointType { double x{}; double y{}; };
struct TextType { std::string s{}; };
struct MyVariant : public std::variant<DoubleType, PointType, TextType>
{
using variant::variant;
constexpr double GetDoubleVal() const
{
return std::visit([](const auto& myValue)
{
// Check whether data member myValue.x exists and is of type double.
// May apply to multiple data types (here: DoubleType and PointType)
// OPEN: why does "std::same_as" not work here?
if constexpr (requires { {myValue.x} -> std::convertible_to<double>; })
{
return myValue.x;
}
return 0.0; // return default value for types having no appropriate data member
}, *this);
}
constexpr std::string GetTextVal() const
{
return std::visit([](const auto& myValue)
{
using T = std::decay_t<decltype(myValue)>;
if constexpr (std::is_same_v<T, TextType>) // explicitly check for a possible DataType
return myValue.s;
return std::string("<not existing>");
}, *this);
}
};
using MyVariants = std::vector<MyVariant>;
Client code:
MyVariants myVariants;
myVariants.emplace_back(DoubleType{ 3.14 });
myVariants.emplace_back(PointType{ 2.34, 1.18 });
myVariants.emplace_back(TextType{ "Hello" });
for (const auto& v : myVariants)
{
std::cout << std::format("doubleVal={} textVal={}\n", v.GetDoubleVal(), v.GetTextVal());
}
Output:
doubleVal=3.14 textVal=<not existing>
doubleVal=2.34 textVal=<not existing>
doubleVal=0 textVal=Hello