diff --git a/doc/variant2/overview.adoc b/doc/variant2/overview.adoc index 0d681b2..b6f5db4 100644 --- a/doc/variant2/overview.adoc +++ b/doc/variant2/overview.adoc @@ -1,5 +1,5 @@ //// -Copyright 2018 Peter Dimov +Copyright 2018, 2019 Peter Dimov Distributed under the Boost Software License, Version 1.0. @@ -11,30 +11,313 @@ http://www.boost.org/LICENSE_1_0.txt # Overview :idprefix: -This library implements a type-safe discriminated union (variant) type, -`variant`, that almost conforms to the {cpp}17 Standard's -http://en.cppreference.com/w/cpp/utility/variant[`std::variant`]. The -main differences between the two are: +This library implements a type-safe discriminated/tagged union type, +`variant`, that is API-compatible with the {cpp}17 Standard's +http://en.cppreference.com/w/cpp/utility/variant[`std::variant`]. -* `variant` does not have a valueless-by-exception state; -* A converting constructor from, e.g. `variant` to - `variant` is provided as an extension; -* The reverse operation, going from `variant` to - `variant` is provided as the member function `subset`. - (This operation can throw if the current state of the variant cannot be - represented.) -* `variant` is not trivial when all contained types are trivial, as - mandated by {cpp}17. +A `variant` variable can hold a value of any of the +types `T1`, `T2`, ..., `Tn`. For example, +`variant` can hold an `int64_t` value, a +`double` value, or a `string` value. -To avoid the valueless-by-exception state, this implementation falls -back to using double storage unless +Such a type is sometimes called a "tagged union", because it's roughly +equivalent to -* one of the alternatives is the type `monostate`, -* one of the alternatives has a nonthrowing default constructor, or -* all the contained types are nothrow move constructible. +``` +struct V +{ + enum tag { tag_int64_t, tag_double, tag_string }; -If the first two bullets don't hold, but the third does, the variant uses -single storage, but `emplace` constructs a temporary and moves it into place -if the construction of the object can throw. In case this is undesirable, one -can force `emplace` into always constructing in-place by adding `monostate` as -one of the alternatives. + tag tag_; + + union + { + int64_t i_; + double d_; + std::string s_; + }; +}; +``` + +Variants can be used to represent dynamically-typed values. A configuration +file of the form + +``` +server.host=test.example.com +server.port=9174 +cache.max_load=0.7 +``` + +can be represented as `std::map>`. + +Variants can also represent polymorphism. To take a classic example, a +polymorphic collection of shapes: + +``` +#define _USE_MATH_DEFINES +#include +#include +#include +#include + +class Shape +{ +public: + + virtual ~Shape() = default; + virtual double area() const = 0; +}; + +class Rectangle: public Shape +{ +private: + + double width_, height_; + +public: + + Rectangle( double width, double height ): + width_( width ), height_( height ) {} + + virtual double area() const { return width_ * height_; } +}; + +class Circle: public Shape +{ +private: + + double radius_; + +public: + + explicit Circle( double radius ): radius_( radius ) {} + virtual double area() const { return M_PI * radius_ * radius_; } +}; + +double total_area( std::vector> const & v ) +{ + double s = 0.0; + + for( auto const& p: v ) + { + s += p->area(); + } + + return s; +} + +int main() +{ + std::vector> v; + + v.push_back( std::unique_ptr( new Circle( 1.0 ) ) ); + v.push_back( std::unique_ptr( new Rectangle( 2.0, 3.0 ) ) ); + + std::cout << "Total area: " << total_area( v ) << std::endl; +} +``` + +can instead be represented as a collection of `variant` +values. This requires the possible `Shape` types be known in advance, as is +often the case. In return, we no longer need virtual functions, or to allocate +the values on the heap with `new Rectangle` and `new Circle`: + +``` +#define _USE_MATH_DEFINES +#include +#include +#include + +#include +using namespace boost::variant2; + +struct Rectangle +{ + double width_, height_; + double area() const { return width_ * height_; } +}; + +struct Circle +{ + double radius_; + double area() const { return M_PI * radius_ * radius_; } +}; + +double total_area( std::vector> const & v ) +{ + double s = 0.0; + + for( auto const& x: v ) + { + s += visit( []( auto const& y ){ return y.area(); }, x ); + } + + return s; +} + +int main() +{ + std::vector> v; + + v.push_back( Circle{ 1.0 } ); + v.push_back( Rectangle{ 2.0, 3.0 } ); + + std::cout << "Total area: " << total_area( v ) << std::endl; +} +``` + +If we look at the + +``` + v.push_back( Circle{ 1.0 } ); +``` + +line, we can deduce that `variant` can be (implicitly) +constructed from `Circle` (and `Rectangle`), and indeed it can. It can also +be assigned a `Circle` or a `Rectangle`: + +``` +variant v = Circle{ 1.0 }; // v holds Circle +v = Rectangle{ 2.0, 3.0 }; // v now holds Rectangle +``` + +If we try to construct `variant` from something that is neither +`int` nor `float`, say, `(short)1`, the behavior is "as if" the `variant` has +declared two constructors, + +``` +variant::variant(int x); +variant::variant(float x); +``` + +and the standard overload resolution rules are used to pick the one that will +be used. So `variant((short)1)` will hold an `int`. + +Putting values into a `variant` is easy, but taking them out is necessarily a +bit more convoluted. It's not possible for `variant` to define a +member function `get() const`, because such a function will need its return +type fixed at compile time, and whether the correct return type is `int` or +`float` will only become known at run time. + +There are a few ways around that. First, there is the accessor member function + +``` +std::size_t variant::index() const noexcept; +``` + +that returns the zero-based index of the current type. For `variant`, it will return `0` for `int` and `1` for `float`. + +Once we have the index, we can use the free function `get` to obtain the +value. Since we're passing the type index to `get`, it knows what to return. +`get<0>(v)` will return `int`, and `get<1>(v)` will return `float`: + +``` +void f( variant const& v ) +{ + switch( v.index() ) + { + case 0: + + // use get<0>(v) + break; + + case 1: + + // use get<1>(v) + break; + + default: + + assert(false); // never happens + } +} +``` + +If we call `get<0>(v)`, and `v.index()` is not currently `0`, an exception +(of type `bad_variant_access`) will be thrown. + +An alternative approach is to use `get(v)` or `get(v)`. This +works similarly. + +Another alternative that avoids the possibility of `bad_variant_access` is +to use `get_if`. Instead of a reference to the contained value, it returns +a pointer to it, returning `nullptr` to indicate type mismatch. `get_if` +takes a pointer to the `variant`, so in our example we'll use something along +the following lines: + +``` +void f( variant const& v ) +{ + if( int const * p = get_if(&v) ) + { + // use *p + } + else if( float const * p = get_if(&v) ) + { + // use *p + } + else + { + assert(false); // never happens + } +} +``` + +Last but not least, there's `visit`. `visit(f, v)` calls the a function object +`f` with the value contained in the `variant` `v` and returns the result. When +`v` is `variant`, it will call `f` with either an `int` or a +`float`. The function object must be prepared to accept both. + +In practice, this can be achieved by having the function take a type that can +be passed either `int` or `float`, such as `double`: + +``` +double f( double x ) { return x; } + +double g( variant const& v ) +{ + return visit( f, v ); +} +``` + +By using a function object with an overloaded `operator()`: + +``` +struct F +{ + void operator()(int x) const { /* use x */ } + void operator()(float x) const { /* use x */ } +}; + +void g( variant const& v ) +{ + visit( F(), v ); +} +``` + +Or by using a polymorphic lambda, as we did in our `Circle`/`Rectangle` +example: + +``` +void g( variant const& v ) +{ + visit( [&]( auto const& x ){ std::cout << x << std::endl; }, v ); +} +``` + +`visit` can also take more than one `variant`. `visit(f, v1, v2)` calls +`f(x1, x2)`, where `x1` is the value contained in `v1` and `x2` is the value +in `v2`. + +The default constructor of `variant` value-initializes the first type in +the list. `variant{}` holds `0` (of type `int`), and +`variant{}` holds `0.0f`. + +This is usually the desired behavior. However, in cases such as +`variant`, one might legitimately wish to +avoid constructing a `std::mutex` by default. A provided type, `monostate`, +can be used as the first type in those scenarios. `variant` will default-construct a `monostate`, +which is basically a no-op, as `monostate` is effectively an empty `struct`.