[variant2] documentation request
Hi Peter, Given that the never-empty guarantee appears to be at the heart of the motivation for boost::variant2, I would suggest to: 1. put in the documentation some motivation behind pursuing the never-empty guarantee. 2. Make it clear that never-empty guarantee is not a strong exception-safety guarantee. 3. Describe the algorithm for achieving the guarantee. The current description does not seem to cover all interesting cases, and I found the behavior quite surprising. For instance, the variant's move assignment will sometimes use T's move assignment rather than move constructor when changing the variant's type, as in the following example: https://wandbox.org/permlink/GDRyS54bpdLP7GVa Regards, Andrzej
Andrzej Krzemienski wrote:
For instance, the variant's move assignment will sometimes use T's move assignment rather than move constructor when changing the variant's type, as in the following example:
Interesting. Since the destructors and the move assignments are trivial, this variant has a pseudo-trivial move constructor, that is, it does the equivalent of memcpy. This however doesn't quite match the specification. I need to look into the latest changes to std::variant to see how it handles triviality in this case.
Describe the algorithm for achieving the [strong] guarantee.
It so happens that the strong guarantee is unachievable with variant (without too much double buffering.) You can either have basic, or noexcept. If all contained types have noexcept move constructors and noexcept move assignments, the move assignment of variant is noexcept. If all types have noexcept move constructors but not all have noexcept move assignments (this can be the case for f.ex. std::vector with a not-always-equal allocator), variant's move assignment is only as strong as the move assignments of the contained types, but emplace is noexcept. If not all types have noexcept move constructors, I don't know of a good way to achieve the strong guarantee.
It so happens that the strong guarantee is unachievable with variant (without too much double buffering.) You can either have basic, or noexcept.
You surprise me, given this is a design capable of double buffering. For a variant2<A, B>, where both A and B have throwing move constructors and assignment, surely if the variant has state A in buffer1, setting it to state B would use buffer2. If B throws during move, we simply don't change the currently selected buffer to buffer2. The variant's A state remains untouched i.e. strong guarantee. I guess you haven't written out anywhere how and when the double buffering comes into play. I would have assumed that as soon as all the types do not have all noexcept moves, the double buffer implementation kicks in. That certainly was the design that Anthony was demonstrating a few years back, unless my memory is faulty. If this isn't the case, that needs to be documented and explained, because I don't see the point of double buffering unless you get the strong guarantee. Niall
Niall Douglas wrote:
It so happens that the strong guarantee is unachievable with variant (without too much double buffering.) You can either have basic, or noexcept.
You surprise me, given this is a design capable of double buffering.
For a variant2<A, B>, where both A and B have throwing move constructors and assignment, surely if the variant has state A in buffer1, setting it to state B would use buffer2. If B throws during move, we simply don't change the currently selected buffer to buffer2. The variant's A state remains untouched i.e. strong guarantee.
I guess you haven't written out anywhere how and when the double buffering comes into play.
That is actually the one thing I have written, twice, once in the README and one in the Overview section of the documentation. :-) "To avoid going into a valueless-by-exception state, this implementation falls back to using double storage unless * 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." So, yes, variant<A, B> will be double-buffered, but variant<monostate, A, B> and variant<int, A, B> won't be.
That is actually the one thing I have written, twice, once in the README and one in the Overview section of the documentation. :-)
"To avoid going into a valueless-by-exception state, this implementation falls back to using double storage unless
* 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."
So, yes, variant<A, B> will be double-buffered, but variant<monostate, A, B> and variant<int, A, B> won't be.
Could we have instead then the following more elemental variants: 1. single_buffered_variant<...>: Never enters a trap state (where valueless_on_exception() = true). Always single buffered. Requires at least one state to have a nothrow default constructor, or all states to have nothrow move constructors. 2. double_buffered_variant<...>: Never enters a trap state (where valueless_on_exception() = true). Always double buffered. Always implements the strong exception guarantee. I would prefer this design because Boost users are more than capable of writing a std::conditional_t<> which chooses what the variant implementation is, based on input types e.g. template<class... Args> using mylocalvariant = std::conditional_t<is_trivially_copyable_for_all<Args...>, single_buffered_variant<Args...>, double_buffered_variant<Args...>>; I personally don't think that you need to choose a hard coded mix of single and double buffered variant. Rather, let the Boost user choose the mix. (But if you're dead set on there being a boost::variant2::variant<...>, let it be a template alias to your current variant mix) Niall
On Sat, Mar 2, 2019 at 1:20 PM Peter Dimov via Boost <boost@lists.boost.org> wrote:
Niall Douglas wrote:
It so happens that the strong guarantee is unachievable with variant (without too much double buffering.) You can either have basic, or noexcept.
You surprise me, given this is a design capable of double buffering.
For a variant2<A, B>, where both A and B have throwing move constructors and assignment, surely if the variant has state A in buffer1, setting it to state B would use buffer2. If B throws during move, we simply don't change the currently selected buffer to buffer2. The variant's A state remains untouched i.e. strong guarantee.
I guess you haven't written out anywhere how and when the double buffering comes into play.
That is actually the one thing I have written, twice, once in the README and one in the Overview section of the documentation. :-)
"To avoid going into a valueless-by-exception state, this implementation falls back to using double storage unless
* 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."
So, yes, variant<A, B> will be double-buffered, but variant<monostate, A, B> and variant<int, A, B> won't be.
For variant<int, short>, can I assign a T that has an operator int()? Is assignment templatized? What happens with struct Bad { operator int() { throw false; } }; variant<int, short> v = (short)10; v = Bad();
Gottlob Frege wrote:
For variant<int, short>, can I assign a T that has an operator int()? Is assignment templatized?
What happens with
struct Bad { operator int() { throw false; } };
variant<int, short> v = (short)10; v = Bad();
For the following: #include "boost/variant2/variant.hpp" #include <iostream> struct Bad { operator int() { throw false; } }; using namespace boost::variant2; int main() { variant<int, short> v = (short)10; try { v = Bad(); } catch(...) { std::cout << "Exception\n"; } visit([](auto const& x){ std::cout << "(" << typeid(x).name() << ")" << x << std::endl; }, v ); } the output is Exception (short)10 There are a number of subtleties here that I haven't quite captured in the documentation; I'll see how I can document the behavior properly. The assignment itself is "template<class U> constexpr variant& operator=( U&& u ) noexcept( /*see below*/ ); Let Tj be a type that is determined as follows: build an imaginary function FUN(Ti) for each alternative type Ti. The overload FUN(Tj) selected by overload resolution for the expression FUN(std::forward<U>(u)) defines the alternative Tj which is the type of the contained value after construction. Effects: If index() == j, assigns std::forward<U>(u) to the value contained in *this. Otherwise, equivalent to emplace<j>(std::forward<U>(u))." and that's correct. So in this case index() is not j and it does emplace<1>( Bad() ). The behavior of emplace varies depending on a number of things: - whether all types are trivially destructible - whether the variant is single- or double-buffered - whether the type we're constructing is nothrow constructible from the arguments - whether all contained types are trivially move constructible and move assignable - and finally, whether, in the single buffered case, we have a monostate type In this specific case, it's trivially destructible, single buffered, int not nothrow constructible from Bar, all types trivially move constructible and move assignable; so we end up constructing a temporary variant, then memcpy-ing it over *this.
On Sun, 3 Mar 2019 at 14:39, Peter Dimov via Boost <boost@lists.boost.org> wrote:
The behavior of emplace varies depending on a number of things:
- whether all types are trivially destructible - whether the variant is single- or double-buffered - whether the type we're constructing is nothrow constructible from the arguments - whether all contained types are trivially move constructible and move assignable - and finally, whether, in the single buffered case, we have a monostate type
Do I need to remember all of the above to correctly use variant2 (and not run into trouble)? degski -- *"Big boys don't cry" - **Eric Stewart, Graham Gouldman*
degski wrote:
On Sun, 3 Mar 2019 at 14:39, Peter Dimov via Boost <boost@lists.boost.org> wrote:
The behavior of emplace varies depending on a number of things:
- whether all types are trivially destructible - whether the variant is single- or double-buffered - whether the type we're constructing is nothrow constructible from the arguments - whether all contained types are trivially move constructible and move assignable - and finally, whether, in the single buffered case, we have a monostate type
Do I need to remember all of the above to correctly use variant2 (and not run into trouble)?
No. The high-level behavior is the same - emplace<i>(args...) replaces the currently held value with Ti(args...), and on exception, the variant holds "a valid but unspecified value".
On Sun, Mar 3, 2019 at 7:39 AM Peter Dimov via Boost <boost@lists.boost.org> wrote:
Gottlob Frege wrote:
For variant<int, short>, can I assign a T that has an operator int()? Is assignment templatized?
What happens with
struct Bad { operator int() { throw false; } };
variant<int, short> v = (short)10; v = Bad();
For the following:
#include "boost/variant2/variant.hpp" #include <iostream>
struct Bad { operator int() { throw false; } };
using namespace boost::variant2;
int main() { variant<int, short> v = (short)10;
try { v = Bad(); } catch(...) { std::cout << "Exception\n"; }
visit([](auto const& x){ std::cout << "(" << typeid(x).name() << ")" << x << std::endl; }, v ); }
the output is
Exception (short)10
There are a number of subtleties here that I haven't quite captured in the documentation; I'll see how I can document the behavior properly. The assignment itself is
"template<class U> constexpr variant& operator=( U&& u ) noexcept( /*see below*/ );
Let Tj be a type that is determined as follows: build an imaginary function FUN(Ti) for each alternative type Ti. The overload FUN(Tj) selected by overload resolution for the expression FUN(std::forward<U>(u)) defines the alternative Tj which is the type of the contained value after construction.
Effects: If index() == j, assigns std::forward<U>(u) to the value contained in *this.
Otherwise, equivalent to emplace<j>(std::forward<U>(u))."
and that's correct. So in this case index() is not j and it does emplace<1>( Bad() ).
The behavior of emplace varies depending on a number of things:
- whether all types are trivially destructible - whether the variant is single- or double-buffered - whether the type we're constructing is nothrow constructible from the arguments - whether all contained types are trivially move constructible and move assignable - and finally, whether, in the single buffered case, we have a monostate type
In this specific case, it's trivially destructible, single buffered, int not nothrow constructible from Bar, all types trivially move constructible and move assignable; so we end up constructing a temporary variant, then memcpy-ing it over *this.
And do you sometimes do the opposite order - memcpy *this to a temporary, emplace into this, memcpy back from the temporary if emplace failed? ie when the current value is trivial, but the new one isn't? I'd like std::variant to do a few more of these heroics.
Gottlob Frege wrote:
And do you sometimes do the opposite order - memcpy *this to a temporary, emplace into this, memcpy back from the temporary if emplace failed?
No, I don't do this. This is a trick that gets us close to the strong guarantee, but not all the way, so it's not worth doing. There are more evil variations, such as memcpy *this, emplace, if failed memcpy back, if not failed memcpy the new *this into another temp, memcpy the original back, invoke destructor, memcpy the new temp back. Totally undefined, of course, but could have worked in practice before compilers started getting clever with their lifetime tracking.
On Sun, Mar 3, 2019 at 7:42 AM Gottlob Frege via Boost < boost@lists.boost.org> wrote:
What happens with
struct Bad { operator int() { throw false; } };
variant<int, short> v = (short)10; v = Bad();
I always considered this ugly corner case that prevents us from having
nice things. Could variant2 use type_traits on = to require that RHS is int or short(optionally + const and & and volatile :) )? I know this will not help with all types(eg std::string or std::vector), but for PODs it would be nice if we could keep out all the exceptions outside variant. Users could still use SometimesBad(that throws sometimes on operator int), but they would need to do do the conversion to int before assignment.
Ivan Matek wrote:
What happens with
struct Bad { operator int() { throw false; } };
variant<int, short> v = (short)10; v = Bad();
I always considered this ugly corner case that prevents us from having nice things.
This is actually not a problem for variant2. When the contained types are trivial, as in this case, the exception, if any, occurs outside the variant. It has to, because otherwise constexpr can't work: https://godbolt.org/z/8kXcBN
On Tue, Mar 5, 2019 at 11:05 PM Peter Dimov via Boost <boost@lists.boost.org> wrote:
Ivan Matek wrote:
What happens with
struct Bad { operator int() { throw false; } };
variant<int, short> v = (short)10; v = Bad();
I always considered this ugly corner case that prevents us from having nice things.
This is actually not a problem for variant2. When the contained types are trivial, as in this case, the exception, if any, occurs outside the variant. It has to, because otherwise constexpr can't work:
Not sure that what this has to do with constexpr, so let me rephrase my question in long but hopefully unambiguous way. First let's forget about variant for a minute: If I have an int of float or std::tuple<int, int, char> I can do anything to instance of that type without any danger of exception( I am sure there are some type_traits/concepts I should mention now, but IDK them by heart). Other kind of types are types like std::string or std::forward_list where some operations( string a, b="Toooo looooooong for SSO"; a *=* b;) might throw. Now when we move to variant of types where each T in list of variant types is some POD(or what is the proper term these days?) I wonder if allowing that variant to throw is a good idea or not? I suspect it is not, if possible library should force user to move throwing stuff outside. For example: struct Bad { int x; operator int() const { if( rand()%10==0) throw float(123.45); return x; } }; variant2<int,short,float> v; v=Bad{5}; I wonder if v=Bad{5}; should be banned by variant since all the Ts are no exception(by this I am talking not about noexcept, but the fact you can do what you want to those types and they will not throw) kind of types. In other words if somebody wants to use variant<int, short, float> with Bad he would need to write v=int(Bad{5}); Now v can never be valuess_by_exception(unless I am missing some other ways to corrupt it). Stated differently I consider the behavior of std::variant unlucky corner case of using perfect forwarding in operator = and emplace(since poor variant ends up ingesting a potential bomb that will throw instead of inspecting it at compile time so he knows it is safe), and I would like to restrict the rhs of operator = in cases when all the types of variant are types that never throw. More specifically I would put some is_same checks here instead of is_assignable(for cases when all types of variant are PODs, so you need a std::conditional also): template<class U, class E1 = typename std::enable_if<!std::is_same<typename std::decay<U>::type, variant>::value>::type, class V = detail::resolve_overload_type<U, T...>, class E2 = typename std::enable_if<std::is_assignable<V&, U>::value && std::is_constructible<V, U>::value>::type > BOOST_CXX14_CONSTEXPR variant& operator=( U&& u ) noexcept( std::is_nothrow_assignable<V&, U>::value && std::is_nothrow_constructible<V, U>::value ) { std::size_t const I = detail::resolve_overload_index<U, T...>::value; if( index() == I ) { _get_impl( mp11::mp_size_t<I>() ) = std::forward<U>(u); } else { this->template emplace<I>( std::forward<U>(u) ); } return *this; } So variant<int, short> v_simple; v_simple = Bad{1}; // does not compile since we can guard against throws inside variant(neither int or short throw) v_simple = int(Bad{1}); // compiles variant<int, std::string> v_complex; v_complex = Bad{1}; // compiles since std::string operator = throws so we can not guard against throws inside variant. regards, Ivan
Andrzej Krzemienski wrote:
For instance, the variant's move assignment will sometimes use T's move assignment rather than move constructor when changing the variant's type, as in the following example:
Interesting.
Since the destructors and the move assignments are trivial, this variant has a pseudo-trivial move constructor, that is, it does the equivalent of memcpy.
This however doesn't quite match the specification.
I need to look into the latest changes to std::variant to see how it handles triviality in this case.
Not surprisingly std::variant gets this correct and only trivially assigns when trivially constructible too. Should be fixed in https://github.com/pdimov/variant2/commit/fdfe9df167ea7a4356691c4c25dc14c1a9..., thanks.
sob., 2 mar 2019 o 15:50 Peter Dimov via Boost <boost@lists.boost.org> napisał(a):
Andrzej Krzemienski wrote:
For instance, the variant's move assignment will sometimes use T's move assignment rather than move constructor when changing the variant's type, as in the following example:
Interesting.
Since the destructors and the move assignments are trivial, this variant has a pseudo-trivial move constructor, that is, it does the equivalent of memcpy.
This however doesn't quite match the specification.
I need to look into the latest changes to std::variant to see how it handles triviality in this case.
Describe the algorithm for achieving the [strong] guarantee.
It so happens that the strong guarantee is unachievable with variant (without too much double buffering.) You can either have basic, or noexcept.
If all contained types have noexcept move constructors and noexcept move assignments, the move assignment of variant is noexcept.
If all types have noexcept move constructors but not all have noexcept move assignments (this can be the case for f.ex. std::vector with a not-always-equal allocator), variant's move assignment is only as strong as the move assignments of the contained types, but emplace is noexcept.
If not all types have noexcept move constructors, I don't know of a good way to achieve the strong guarantee.
Technically, it could be possible with unconditional double buffering, but I guess the cost is not worth it. My point is, users who will read the introductory part of the documentation will get an incorrect impression that variant2 already offers a strong exception safety guarantee. The docs never state that, but when you read the clever things what this library does, some users will think, "surely this must be to achieve the strong guarantee". A not that states explicitly that this is not a strong guarantee would help avoid this confusion. Regards, Andrzej
My point is, users who will read the introductory part of the documentation will get an incorrect impression that variant2 already offers a strong exception safety guarantee. The docs never state that, but when you read the clever things what this library does, some users will think, "surely this must be to achieve the strong guarantee". A not that states explicitly that this is not a strong guarantee would help avoid this confusion.
This is why I would suggest variant2 provide elemental single_buffer_variant and double_buffer_variant, and let the end user choose which they want. Like how Outcome's result<T, E> is a template alias to basic_result. Niall
niedz., 3 mar 2019 o 21:27 Niall Douglas via Boost <boost@lists.boost.org> napisał(a):
My point is, users who will read the introductory part of the documentation will get an incorrect impression that variant2 already offers a strong exception safety guarantee. The docs never state that, but when you read the clever things what this library does, some users will think, "surely this must be to achieve the strong guarantee". A not that states explicitly that this is not a strong guarantee would help avoid this confusion.
This is why I would suggest variant2 provide elemental single_buffer_variant and double_buffer_variant, and let the end user choose which they want.
I would take this idea further (or rather in a different direction), and say that you never need double_buffer_variant for automatic objects used locally. For global objects (or global-like) you need double buffering not only for variants, but probably for every type that does not offer strong guarantee on its operations, so what you need is a generic double-buffer wrapper. Regards, Andrzej
Niall Douglas wrote:
This is why I would suggest variant2 provide elemental single_buffer_variant and double_buffer_variant, and let the end user choose which they want.
variant2 presently is single_buffer_variant except that instead of giving you an error when that's not possible, it silently switches to double_buffer_variant and soldiers on. (Which happens rarely.) I agree that one can make a good argument for (a variation of) double_buffer_variant, which prioritizes strong guarantee over sizeof. But that's only needed when your contained types don't have noexcept move. In this case, a not unreasonable course of action is to hold them by unique_ptr in the variant instead.
On 4/03/2019 10:43, Peter Dimov wrote:
Niall Douglas wrote:
This is why I would suggest variant2 provide elemental single_buffer_variant and double_buffer_variant, and let the end user choose which they want.
variant2 presently is single_buffer_variant except that instead of giving you an error when that's not possible, it silently switches to double_buffer_variant and soldiers on. (Which happens rarely.)
I dislike in general silent implementation switches that can have performance consequences that can be triggered by a typo or a forgetful lack of "noexcept" modifier.
I agree that one can make a good argument for (a variation of) double_buffer_variant, which prioritizes strong guarantee over sizeof. But that's only needed when your contained types don't have noexcept move. In this case, a not unreasonable course of action is to hold them by unique_ptr in the variant instead.
How hard would it be to publicly provide single_buffer_variant, double_buffer_variant, and variant (where the latter is just an intelligent alias to one of the other two based on the argument types, as per your current rules)? That would let consumers who care explicitly indicate that they don't want double-buffering -- and then get a compile error if they inadvertently put an incorrect type in, rather than getting unintended buffering. (I'm less sure if there's a use case for explicitly wanting double-buffering always, but it seems easier to provide it than not to, if you have to write a double-buffered version anyway.) But the alias means that consumers who don't care, don't have to.
This is why I would suggest variant2 provide elemental single_buffer_variant and double_buffer_variant, and let the end user choose which they want.
variant2 presently is single_buffer_variant except that instead of giving you an error when that's not possible, it silently switches to double_buffer_variant and soldiers on. (Which happens rarely.)
I agree that one can make a good argument for (a variation of) double_buffer_variant, which prioritizes strong guarantee over sizeof. But that's only needed when your contained types don't have noexcept move. In this case, a not unreasonable course of action is to hold them by unique_ptr in the variant instead.
Except I don't want to pay for the dynamic memory allocation.
From my perspective, there is no downside to exposing directly to the Boost user both single and double buffered implementations.
Let the Boost user decide what tradeoffs they prefer. Niall
Niall Douglas wrote:
I agree that one can make a good argument for (a variation of) double_buffer_variant, which prioritizes strong guarantee over sizeof. But that's only needed when your contained types don't have noexcept move. In this case, a not unreasonable course of action is to hold them by unique_ptr in the variant instead.
Except I don't want to pay for the dynamic memory allocation.
Types that don't have noexcept move typically already allocate. Yes, it's an extra allocation, but going from 1 to 2 is not the same as going from 0 to 1.
I agree that one can make a good argument for (a variation of) > double_buffer_variant, which prioritizes strong guarantee over sizeof. But that's only needed when your contained types don't have noexcept move. In this case, a not unreasonable course of action is to hold them > by unique_ptr in the variant instead.
Except I don't want to pay for the dynamic memory allocation.
Types that don't have noexcept move typically already allocate. Yes, it's an extra allocation, but going from 1 to 2 is not the same as going from 0 to 1.
You're thinking of modern code, or code you are permitted to change. I'm thinking of code that I am not permitted to change, whose moves are not noexcept because somebody forgot to mark them, and it's now written into stone for the next five years. I face a raft of such code regularly. I suspect I am not alone amongst Boost users. You seem very wedded to not breaking out single_buffer_variant and double_buffer_variant Peter. You seem keen we should accept your preferred mix of when each ought to be employed using your hardcoded logic. Can I ask why? Niall
On Mon, Mar 4, 2019 at 7:46 AM Niall Douglas via Boost < boost@lists.boost.org> wrote:
I agree that one can make a good argument for (a variation of) > double_buffer_variant, which prioritizes strong guarantee over sizeof. But that's only needed when your contained types don't have noexcept move. In this case, a not unreasonable course of action is to hold them > by unique_ptr in the variant instead.
Except I don't want to pay for the dynamic memory allocation.
Types that don't have noexcept move typically already allocate. Yes, it's an extra allocation, but going from 1 to 2 is not the same as going from 0 to 1.
You're thinking of modern code, or code you are permitted to change.
I'm thinking of code that I am not permitted to change, whose moves are not noexcept because somebody forgot to mark them, and it's now written into stone for the next five years.
I've heard that some standard libraries used to define such move constructors. This, by the way, is proof positive that noexcept is defective: the thing that specifies that something can't throw is, practically speaking, useless in checking if something may throw.
On Mon, 4 Mar 2019 at 20:00, Emil Dotchevski via Boost < boost@lists.boost.org> wrote:
This, by the way, is proof positive that noexcept is defective: the thing that specifies that something can't throw is, practically speaking, useless in checking if something may throw.
There's no need for proof of its *defectiveness for the purpose* you would like to use it: "Note that a *noexcept* specification on a function is not a compile-time check; it is merely a method for a programmer to inform the compiler whether or not a function should throw exceptions." *) degski *) https://en.cppreference.com/w/cpp/language/noexcept_spec -- *"Big boys don't cry" - **Eric Stewart, Graham Gouldman*
On Mon, Mar 4, 2019 at 10:27 PM degski via Boost <boost@lists.boost.org> wrote:
On Mon, 4 Mar 2019 at 20:00, Emil Dotchevski via Boost < boost@lists.boost.org> wrote:
This, by the way, is proof positive that noexcept is defective: the thing that specifies that something can't throw is, practically speaking, useless in checking if something may throw.
There's no need for proof of its *defectiveness for the purpose* you would like to use it: "Note that a *noexcept* specification on a function is not a compile-time check; it is merely a method for a programmer to inform the compiler whether or not a function should throw exceptions." *)
Yes I am aware of the semantics of noexcept. The point is, "I'm thinking of code that I am not permitted to change, whose moves are not noexcept because somebody forgot to mark them, and it's now written into stone for the next five years." indicates a problem with the noexcept definition. On the other hand, statically enforced noexcept would have other problems. I don't think it can be fixed.
Niall Douglas wrote:
You seem very wedded to not breaking out single_buffer_variant and double_buffer_variant Peter. You seem keen we should accept your preferred mix of when each ought to be employed using your hardcoded logic. Can I ask why?
I don't have a good answer to this question. Breaking out into two separate classes per number of buffers seems sensible and straightforward, and I don't have any waterproof arguments against it apart from the fact that I intuitively dislike it. I prefer having a single variant, named "variant", which is almost always a drop-in replacement for std::variant, for the fairly obvious reason that (a) it's easy to recommend to users: just use variant2::variant instead of std::variant, and your problems will be gone (not valid in all jurisdictions) and (b) it would be possible, in theory, to one day replace std::variant with it. For people who'd rather have single_buffer_variant<T...>, I can provide a static constexpr member function `bool variant<T...>::is_single_buffered()`, which they can static_assert. So template<class... T> using single_buffer_variant = mp_if_c<variant<T...>::is_single_buffered(), variant<T...>>; To those who'd rather have double_buffer_variant, though, I don't have a good offer.
AMDG On 3/4/19 5:44 PM, Peter Dimov via Boost wrote:
Niall Douglas wrote:
You seem very wedded to not breaking out single_buffer_variant and double_buffer_variant Peter. You seem keen we should accept your preferred mix of when each ought to be employed using your hardcoded logic. Can I ask why?
I don't have a good answer to this question. Breaking out into two separate classes per number of buffers seems sensible and straightforward, and I don't have any waterproof arguments against it apart from the fact that I intuitively dislike it.
I know why I dislike it. It exposes low level details of the implementation which most users should neither know nor care about. If someone asks for a single buffered variant, what he really means, is "I really care about performance. Give me a hard error instead of using more space." double_buffered_variant, really means "give me strong exception safety. I don't care about the cost." Exposing single_buffered_variant and double_buffered_variant effectively rules out other possible compromises. For example, if all the types without no-throw move are smaller than the largest type in the variant, then you can save space with a partially double buffered variant.
I prefer having a single variant, named "variant", which is almost always a drop-in replacement for std::variant, for the fairly obvious reason that (a) it's easy to recommend to users: just use variant2::variant instead of std::variant, and your problems will be gone (not valid in all jurisdictions) and (b) it would be possible, in theory, to one day replace std::variant with it.
For people who'd rather have single_buffer_variant<T...>, I can provide a static constexpr member function `bool variant<T...>::is_single_buffered()`, which they can static_assert. So
template<class... T> using single_buffer_variant = mp_if_c<variant<T...>::is_single_buffered(), variant<T...>>;
To those who'd rather have double_buffer_variant, though, I don't have a good offer.
In Christ, Steven Watanabe
I know why I dislike it. It exposes low level details of the implementation which most users should neither know nor care about. If someone asks for a single buffered variant, what he really means, is "I really care about performance. Give me a hard error instead of using more space."
Not quite. It means "I understand and want the tradeoffs that using a single buffer (and no dynamic allocation) requires".
double_buffered_variant, really means "give me strong exception safety. I don't care about the cost."
Not quite. It means "I don't trust that somebody won't mess up this variant during code maintenance in the next decade".
Exposing single_buffered_variant and double_buffered_variant effectively rules out other possible compromises.
I think that shows a lack of imagination. For example, why can't double_buffered_variant be constructed by gluing two single_buffered_variants together internally, and wrapping the glue with logic to switch between them in an exception safe fashion? If that's not possible, it smells to me like that's a design issue which needs fixing. Elementary components ought to be composable. I don't claim that this is easy to get right, but I do claim it is worth thinking about. All that said, you do raise a very valid point important to what is becoming a fairly comprehensive design review discussion already. What is lost by taking a "composition of elementals" design approach?
For
example, if all the types without no-throw move are smaller than the largest type in the variant, then you can save space with a partially double buffered variant.
I'd consider that sort of argument as important if variant2 implements or would implement that kind of optimisation. If variant2 doesn't implement that kind of optimisation, and won't, it holds much less weight with me. i.e. Peter what specific gains do you see as important that you want to retain a single variant implementation with hardcoded selection logic? (I might remind Boost now of Outcome. As presented, it had a hardcoded logic selecting between configurations. You guys persuaded me to break out the internal implementations, expose them to the user, and make result<T, E> a template alias to a preconfigured mix of those internals. I didn't like the idea at the time one bit. But after I did the substantial refactor, and deployed it into production, it's grown on me. To the extent that I now recognise that I was wrong, and you guys were right. And I'm wondering the same about variant2) Niall
Niall Douglas wrote:
double_buffered_variant, really means "give me strong exception safety. I don't care about the cost."
Not quite. It means "I don't trust that somebody won't mess up this variant during code maintenance in the next decade".
Your reformulation aside, "give me the strong guarantee" is exactly what it means, and double buffering is only means to that end and insufficient in itself. A strong guarantee variant must never delegate to the assignment operator of the contained type; it has to destroy and recreate even when the types match. I have argued for this behavior (for std::variant and std::optional) in the past. Aside from delivering the strong guarantee, it can also be used to make a non-assignable type assignable by wrapping it in a variant<> or optional<>. But there's too much opposition and it's never going to be accepted for std::variant or std::optional. The strong guarantee is somewhat of an annoyance. It's almost never needed, and it doesn't compose. If you have two types T and U, which offer the basic guarantee on assignment, so does struct { T t; U u; }. And if they offer the noexcept guarantee on assignment, so does the struct. But if they offer the strong guarantee on assignment, the struct doesn't, and there's no way to make it so (using only the provided strong assignment; if you have noexcept swap, you can, but that's because it's noexcept, a strong swap would similarly be useless). So the strong guarantee is almost always the wrong thing to provide. You want basic for the copy assignment, and noexcept for the move assignment. And people who argue against strong for variant<> have a point.
sob., 2 mar 2019 o 13:18 Andrzej Krzemienski <akrzemi1@gmail.com> napisał(a):
Hi Peter, Given that the never-empty guarantee appears to be at the heart of the motivation for boost::variant2, I would suggest to: 1. put in the documentation some motivation behind pursuing the never-empty guarantee. 2. Make it clear that never-empty guarantee is not a strong exception-safety guarantee. 3. Describe the algorithm for achieving the guarantee.
Somewhat related to that. When I try to study the documentation to see what the assignment guarantees, I get this: constexpr variant& operator=( const variant& r ) noexcept( mp_all<std::is_nothrow_copy_constructible<T>..., std::is_nothrow_copy_assignable<T>...>::value ); - Let j be r.index(). Effects: - If index() == j, assigns the value contained in r to the value contained in *this. - Otherwise, equivalent to emplace<j>(get<j>(r)). I am interested in the latter ("Otherwise") case, so, going to emplace: template<size_t I, class... A> constexpr variant_alternative_t<I, variant<T...>>& emplace( A&&... a ); - Requires: I < sizeof(T…). Effects: Destroys the currently contained value, then initializes a new contained value as if using the expression Ti(std::forward<A>(a)…). This doesn't mention any double buffering. No tricks, no "monostate". It reads as if the variant is left with no contained object whatsoever. Regards, Andrzej
Andrzej Krzemienski wrote:
I am interested in the latter ("Otherwise") case, so, going to emplace:
template<size_t I, class... A> constexpr variant_alternative_t<I, variant<T...>>& emplace( A&&... a );
- Requires: I < sizeof(T…).
Effects: Destroys the currently contained value, then initializes a new contained value as if using the expression Ti(std::forward<A>(a)…).
This doesn't mention any double buffering. No tricks, no "monostate". It reads as if the variant is left with no contained object whatsoever.
Yes, we mentioned that upthread. At minimum, I'll need to add Remarks that on exception, the variant is left in a valid but unspecified state. Rigorously specifying the exact behavior of emplace will be rather verbose, and I'm not yet sure how to go about it. What's important about the assignment guarantees is this:
noexcept( mp_all<std::is_nothrow_copy_constructible<T>..., std::is_nothrow_copy_assignable<T>...>::value );
or, more practically, this:
constexpr variant& operator=( variant&& r ) noexcept( mp_all<std::is_nothrow_move_constructible<T>..., std::is_nothrow_move_assignable<T>...>::value );
That is, the way to achieve the strong guarantee on assignment is to have all type nothrow moveable, and then using v1= V(v2);
śr., 6 mar 2019 o 17:39 Peter Dimov via Boost <boost@lists.boost.org> napisał(a):
Andrzej Krzemienski wrote:
I am interested in the latter ("Otherwise") case, so, going to emplace:
template<size_t I, class... A> constexpr variant_alternative_t<I, variant<T...>>& emplace( A&&... a );
- Requires: I < sizeof(T…).
Effects: Destroys the currently contained value, then initializes a new contained value as if using the expression Ti(std::forward<A>(a)…).
This doesn't mention any double buffering. No tricks, no "monostate". It reads as if the variant is left with no contained object whatsoever.
Yes, we mentioned that upthread. At minimum, I'll need to add Remarks that on exception, the variant is left in a valid but unspecified state.
Rigorously specifying the exact behavior of emplace will be rather verbose, and I'm not yet sure how to go about it.
There is a number of tricks to put back the variant to a non-empty state after a throw, and you may wish to reserve the right to change them in the future (and guarantee only that one of the Ts is stored with a valid but unspecified value). However, it looks like you also want to guarantee some smaller things: * If one of the types is `monostate` you guarantee that upon throw, the type stored is `monostate`. * If at least one type satisfies the noexcept requirements you guarantee that the size of variant<T...> is no more that the biggest of T plus the discriminator. Regards, Andrzej
participants (10)
-
Andrzej Krzemienski
-
degski
-
Emil Dotchevski
-
Gavin Lambert
-
Gottlob Frege
-
Ivan Matek
-
Niall Douglas
-
Peter Dimov
-
Peter Dimov
-
Steven Watanabe