forked from boostorg/variant2
Update overview
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
////
|
////
|
||||||
Copyright 2018 Peter Dimov
|
Copyright 2018, 2019 Peter Dimov
|
||||||
|
|
||||||
Distributed under the Boost Software License, Version 1.0.
|
Distributed under the Boost Software License, Version 1.0.
|
||||||
|
|
||||||
@@ -11,30 +11,313 @@ http://www.boost.org/LICENSE_1_0.txt
|
|||||||
# Overview
|
# Overview
|
||||||
:idprefix:
|
:idprefix:
|
||||||
|
|
||||||
This library implements a type-safe discriminated union (variant) type,
|
This library implements a type-safe discriminated/tagged union type,
|
||||||
`variant<T...>`, that almost conforms to the {cpp}17 Standard's
|
`variant<T...>`, that is API-compatible with the {cpp}17 Standard's
|
||||||
http://en.cppreference.com/w/cpp/utility/variant[`std::variant<T...>`]. The
|
http://en.cppreference.com/w/cpp/utility/variant[`std::variant<T...>`].
|
||||||
main differences between the two are:
|
|
||||||
|
|
||||||
* `variant<T...>` does not have a valueless-by-exception state;
|
A `variant<T1, T2, ..., Tn>` variable can hold a value of any of the
|
||||||
* A converting constructor from, e.g. `variant<int, float>` to
|
types `T1`, `T2`, ..., `Tn`. For example,
|
||||||
`variant<float, double, int>` is provided as an extension;
|
`variant<int64_t, double, std::string>` can hold an `int64_t` value, a
|
||||||
* The reverse operation, going from `variant<float, double, int>` to
|
`double` value, or a `string` value.
|
||||||
`variant<int, float>` is provided as the member function `subset<U...>`.
|
|
||||||
(This operation can throw if the current state of the variant cannot be
|
|
||||||
represented.)
|
|
||||||
* `variant<T...>` is not trivial when all contained types are trivial, as
|
|
||||||
mandated by {cpp}17.
|
|
||||||
|
|
||||||
To avoid the valueless-by-exception state, this implementation falls
|
Such a type is sometimes called a "tagged union", because it's roughly
|
||||||
back to using double storage unless
|
equivalent to
|
||||||
|
|
||||||
* one of the alternatives is the type `monostate`,
|
```
|
||||||
* one of the alternatives has a nonthrowing default constructor, or
|
struct V
|
||||||
* all the contained types are nothrow move constructible.
|
{
|
||||||
|
enum tag { tag_int64_t, tag_double, tag_string };
|
||||||
|
|
||||||
If the first two bullets don't hold, but the third does, the variant uses
|
tag tag_;
|
||||||
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
|
union
|
||||||
can force `emplace` into always constructing in-place by adding `monostate` as
|
{
|
||||||
one of the alternatives.
|
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<std::string, variant<int64_t, double,
|
||||||
|
std::string>>`.
|
||||||
|
|
||||||
|
Variants can also represent polymorphism. To take a classic example, a
|
||||||
|
polymorphic collection of shapes:
|
||||||
|
|
||||||
|
```
|
||||||
|
#define _USE_MATH_DEFINES
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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<std::unique_ptr<Shape>> const & v )
|
||||||
|
{
|
||||||
|
double s = 0.0;
|
||||||
|
|
||||||
|
for( auto const& p: v )
|
||||||
|
{
|
||||||
|
s += p->area();
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
std::vector<std::unique_ptr<Shape>> v;
|
||||||
|
|
||||||
|
v.push_back( std::unique_ptr<Shape>( new Circle( 1.0 ) ) );
|
||||||
|
v.push_back( std::unique_ptr<Shape>( new Rectangle( 2.0, 3.0 ) ) );
|
||||||
|
|
||||||
|
std::cout << "Total area: " << total_area( v ) << std::endl;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
can instead be represented as a collection of `variant<Rectangle, Circle>`
|
||||||
|
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 <iostream>
|
||||||
|
#include <vector>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include <boost/variant2/variant.hpp>
|
||||||
|
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<variant<Rectangle, Circle>> 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<variant<Rectangle, Circle>> 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<Rectangle, Circle>` can be (implicitly)
|
||||||
|
constructed from `Circle` (and `Rectangle`), and indeed it can. It can also
|
||||||
|
be assigned a `Circle` or a `Rectangle`:
|
||||||
|
|
||||||
|
```
|
||||||
|
variant<Rectangle, Circle> v = Circle{ 1.0 }; // v holds Circle
|
||||||
|
v = Rectangle{ 2.0, 3.0 }; // v now holds Rectangle
|
||||||
|
```
|
||||||
|
|
||||||
|
If we try to construct `variant<int, float>` 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<int, float>((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<int, float>` 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<int,
|
||||||
|
float>`, it will return `0` for `int` and `1` for `float`.
|
||||||
|
|
||||||
|
Once we have the index, we can use the free function `get<N>` 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<int, float> 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<int>(v)` or `get<float>(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<int, float> const& v )
|
||||||
|
{
|
||||||
|
if( int const * p = get_if<int>(&v) )
|
||||||
|
{
|
||||||
|
// use *p
|
||||||
|
}
|
||||||
|
else if( float const * p = get_if<float>(&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<int, float>`, 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<int, float> 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<int, float> const& v )
|
||||||
|
{
|
||||||
|
visit( F(), v );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or by using a polymorphic lambda, as we did in our `Circle`/`Rectangle`
|
||||||
|
example:
|
||||||
|
|
||||||
|
```
|
||||||
|
void g( variant<int, float> 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<int, float>{}` holds `0` (of type `int`), and
|
||||||
|
`variant<float, int>{}` holds `0.0f`.
|
||||||
|
|
||||||
|
This is usually the desired behavior. However, in cases such as
|
||||||
|
`variant<std::mutex, std::recursive_mutex>`, 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<monostate,
|
||||||
|
std::mutex, std::recursive_mutex>` will default-construct a `monostate`,
|
||||||
|
which is basically a no-op, as `monostate` is effectively an empty `struct`.
|
||||||
|
Reference in New Issue
Block a user