Update overview

This commit is contained in:
Peter Dimov
2019-05-11 21:06:31 +03:00
parent c138488551
commit 6f190133be

View File

@@ -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<T...>`, that almost conforms to the {cpp}17 Standard's
http://en.cppreference.com/w/cpp/utility/variant[`std::variant<T...>`]. The
main differences between the two are:
This library implements a type-safe discriminated/tagged union type,
`variant<T...>`, that is API-compatible with the {cpp}17 Standard's
http://en.cppreference.com/w/cpp/utility/variant[`std::variant<T...>`].
* `variant<T...>` does not have a valueless-by-exception state;
* A converting constructor from, e.g. `variant<int, float>` to
`variant<float, double, int>` is provided as an extension;
* The reverse operation, going from `variant<float, double, int>` to
`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.
A `variant<T1, T2, ..., Tn>` variable can hold a value of any of the
types `T1`, `T2`, ..., `Tn`. For example,
`variant<int64_t, double, std::string>` 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<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`.