[variant2] Andrzej's review -- design
Hi Everyone,
First of all, I wanted to thank Peter for writing this non-trivial library
and sharing it with the community.
This is not a full review yet. I just wanted to discuss the design first,
especially the never-empty guarantee. And I am going to rehash the old
argument.
The core of variant2 is its never-empty guarantee and the mechanism to
achieve it: a number of different algorithms chosen depending on what the
potentially stored types permit. In fact this mechanism is not much
different than the one present in boost::variant.
The issue to solve is what happens in copy/move assignment of variant
Andrzej Krzemienski wrote:
It should be noted that variant2 DOES NOT PROVIDE A STRONG EXCEPTION SAFETY GUARANTEE.
Yes, variant2::variant is a never-valueless, basic guarantee variant. There probably does exist a legitimate need for a strong guarantee variant, which would have to make different design tradeoffs. It's certainly possible to add one in the future, if there's demand. (For instance, a "strong" variant wouldn't delegate to T::op= in its assignment, from which it falls out that it would be able to store non-assignable types and still be assignable itself. It would double-buffer much more often though.)
It should be noted that variant2 DOES NOT PROVIDE A STRONG EXCEPTION SAFETY GUARANTEE. The never-empty guarantee has a certain negative effect of confusing people so that they are led to believe that they have strong exception safety. But they don't. even expert programmers in this forum expressed their surprise that changing from X to Y in variant
can get them a variant storing Z. Sometimes it is even trickier: X may be a container that currently stores 10 elements. You assign Y, and after the exception you get an X that stores zero elements. Because of these problems, the only reasonable thing to do is to follow the advice from Dave Abraham's article: https://www.boost.org/community/exception_safety.html To quote the relevant part, "If a component has many valid states, after an exception we have no idea what state the component is in; only that the state is valid. The options for recovery in this case are limited: either destruction or resetting the component to some known state before further use." If this principle is followed no-one will ever observe the valueless_by_exception state. This also applies to variant2. If this principle from Dave Abrahams is followed, no-one will ever benefit from
On Tue, Apr 2, 2019 at 9:28 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote: the
never-empty guarantee (but people will still pay the costs of providing it). You cannot do anything meaningful with variant that threw other than destroy or reset it. (But it is probably best to destroy it.) If objects are allowed to survive the stack unwinding, they should either offer a strong exception safety guarantee, or you have to have some very detailed knowledge of a given particular type how its state gets modified under exceptions, but this is usually very difficult and best left to experts.
The argument for a variant type that never ends up in a "valueless" state is not that it is wonderful to deal with objects that change state from X to Y, but that error recovery code is already difficult to write correctly, and while dealing with an object found in a state we didn't leave it in (basic exception safety guarantee) may be annoying, this is preferable than having to deal with objects that explode unless they're destroyed. It's not that programmers "want" to do something with the object other than destroying it, but 1) it is difficult to ensure that this can never ever happen and 2) the way it can be ensured is to sprinkle checks at random places (e.g. where variant asserts are triggered when during error recovery some code happens to touch an object but not destroy it), which is even worse.
wt., 2 kwi 2019 o 19:23 Emil Dotchevski via Boost
It should be noted that variant2 DOES NOT PROVIDE A STRONG EXCEPTION SAFETY GUARANTEE. The never-empty guarantee has a certain negative effect of confusing people so that they are led to believe that they have strong exception safety. But they don't. even expert programmers in this forum expressed their surprise that changing from X to Y in variant
can get them a variant storing Z. Sometimes it is even trickier: X may be a container that currently stores 10 elements. You assign Y, and after the exception you get an X that stores zero elements. Because of these problems, the only reasonable thing to do is to follow the advice from Dave Abraham's article: https://www.boost.org/community/exception_safety.html To quote the relevant part, "If a component has many valid states, after an exception we have no idea what state the component is in; only that the state is valid. The options for recovery in this case are limited: either destruction or resetting the component to some known state before further use." If this principle is followed no-one will ever observe the valueless_by_exception state. This also applies to variant2. If this principle from Dave Abrahams is followed, no-one will ever benefit from
On Tue, Apr 2, 2019 at 9:28 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote: the
never-empty guarantee (but people will still pay the costs of providing it). You cannot do anything meaningful with variant that threw other than destroy or reset it. (But it is probably best to destroy it.) If objects are allowed to survive the stack unwinding, they should either offer a strong exception safety guarantee, or you have to have some very detailed knowledge of a given particular type how its state gets modified under exceptions, but this is usually very difficult and best left to experts.
The argument for a variant type that never ends up in a "valueless" state is not that it is wonderful to deal with objects that change state from X to Y, but that error recovery code is already difficult to write correctly, and while dealing with an object found in a state we didn't leave it in (basic exception safety guarantee) may be annoying, this is preferable than having to deal with objects that explode unless they're destroyed. It's not that programmers "want" to do something with the object other than destroying it, but 1) it is difficult to ensure that this can never ever happen and 2) the way it can be ensured is to sprinkle checks at random places (e.g. where variant asserts are triggered when during error recovery some code happens to touch an object but not destroy it), which is even worse.
Please, illustrate it with an example. Maybe you have some good scenario in mind, but to me, the necessity of sprinkling checks at various places is an indication of a bad approach to exception handling. If I just unwind the stack and let all my objects die, there is no point where I would need any checks. Regards, &rzej;
On Tue, Apr 2, 2019 at 11:53 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
wt., 2 kwi 2019 o 19:23 Emil Dotchevski via Boost
napisał(a): On Tue, Apr 2, 2019 at 9:28 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
It should be noted that variant2 DOES NOT PROVIDE A STRONG EXCEPTION SAFETY GUARANTEE. The never-empty guarantee has a certain negative effect of confusing people so that they are led to believe that they have strong exception safety. But they don't. even expert programmers in this
expressed their surprise that changing from X to Y in variant
can get them a variant storing Z. Sometimes it is even trickier: X may be a container that currently stores 10 elements. You assign Y, and after
exception you get an X that stores zero elements. Because of these problems, the only reasonable thing to do is to follow the advice from Dave Abraham's article: https://www.boost.org/community/exception_safety.html To quote the relevant part, "If a component has many valid states, after an exception we have no idea what state the component is in; only that
state is valid. The options for recovery in this case are limited: either destruction or resetting the component to some known state before further use."
If this principle is followed no-one will ever observe the valueless_by_exception state. This also applies to variant2. If this principle from Dave Abrahams is followed, no-one will ever benefit from the never-empty guarantee (but people will still pay the costs of
it). You cannot do anything meaningful with variant that threw other
destroy or reset it. (But it is probably best to destroy it.) If objects are allowed to survive the stack unwinding, they should either offer a strong exception safety guarantee, or you have to have some very detailed knowledge of a given particular type how its state gets modified under exceptions, but this is usually very difficult and best left to experts.
The argument for a variant type that never ends up in a "valueless" state is not that it is wonderful to deal with objects that change state from X to Y, but that error recovery code is already difficult to write correctly, and while dealing with an object found in a state we didn't leave it in (basic exception safety guarantee) may be annoying, this is preferable
forum the the providing than than
having to deal with objects that explode unless they're destroyed. It's not that programmers "want" to do something with the object other than destroying it, but 1) it is difficult to ensure that this can never ever happen and 2) the way it can be ensured is to sprinkle checks at random places (e.g. where variant asserts are triggered when during error recovery some code happens to touch an object but not destroy it), which is even worse.
Please, illustrate it with an example. Maybe you have some good scenario in mind, but to me, the necessity of sprinkling checks at various places is an indication of a bad approach to exception handling.
Aren't you asking for an example of a destructor that does something other than call other destructors?
wt., 2 kwi 2019 o 21:31 Emil Dotchevski via Boost
On Tue, Apr 2, 2019 at 11:53 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
wt., 2 kwi 2019 o 19:23 Emil Dotchevski via Boost
napisał(a):
On Tue, Apr 2, 2019 at 9:28 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
It should be noted that variant2 DOES NOT PROVIDE A STRONG EXCEPTION SAFETY GUARANTEE. The never-empty guarantee has a certain negative effect of confusing people so that they are led to believe that they have
exception safety. But they don't. even expert programmers in this forum expressed their surprise that changing from X to Y in variant
can get them a variant storing Z. Sometimes it is even trickier: X may be a container that currently stores 10 elements. You assign Y, and after exception you get an X that stores zero elements. Because of these problems, the only reasonable thing to do is to follow the advice from Dave Abraham's article: https://www.boost.org/community/exception_safety.html To quote the relevant part, "If a component has many valid states, after an exception we have no idea what state the component is in; only that
state is valid. The options for recovery in this case are limited: either destruction or resetting the component to some known state before further use."
If this principle is followed no-one will ever observe the valueless_by_exception state. This also applies to variant2. If this principle from Dave Abrahams is followed, no-one will ever benefit from the never-empty guarantee (but people will still pay the costs of
it). You cannot do anything meaningful with variant that threw other
destroy or reset it. (But it is probably best to destroy it.) If objects are allowed to survive the stack unwinding, they should either offer a strong exception safety guarantee, or you have to have some very detailed knowledge of a given particular type how its state gets modified under exceptions, but this is usually very difficult and best left to experts.
The argument for a variant type that never ends up in a "valueless" state is not that it is wonderful to deal with objects that change state from X to Y, but that error recovery code is already difficult to write correctly, and while dealing with an object found in a state we didn't leave it in (basic exception safety guarantee) may be annoying, this is preferable
strong the the providing than than
having to deal with objects that explode unless they're destroyed. It's not that programmers "want" to do something with the object other than destroying it, but 1) it is difficult to ensure that this can never ever happen and 2) the way it can be ensured is to sprinkle checks at random places (e.g. where variant asserts are triggered when during error recovery some code happens to touch an object but not destroy it), which is even worse.
Please, illustrate it with an example. Maybe you have some good scenario in mind, but to me, the necessity of sprinkling checks at various places is an indication of a bad approach to exception handling.
Aren't you asking for an example of a destructor that does something other than call other destructors?
Can you show a destructor like that, does a visitatoin on a variant, and that you wouldn't be ashamed to put in your program? Regards, &rzej;
Andrzej Krzemienski wrote:
Can you show a destructor like that, does a visitatoin on a variant, and that you wouldn't be ashamed to put in your program?
The destructor doesn't have to do visitation. It merely needs to call a function f1, which calls a function f2, which calls a function f3, which calls a function f4, which does visitation. What we've been telling you from the start is that this forces you to partition your functions into two classes, one allowed to do visitation, the other not, then keep track of which is which, never calling the wrong class in a destructor, or from a catch clause. And, of course, this has nothing to do with visitation, specifically. The exception safety guarantee is a global thing. Once you lose basic, you lose it everywhere, not just for visit.
wt., 2 kwi 2019 o 22:59 Peter Dimov via Boost
Andrzej Krzemienski wrote:
Can you show a destructor like that, does a visitatoin on a variant, and that you wouldn't be ashamed to put in your program?
The destructor doesn't have to do visitation. It merely needs to call a function f1, which calls a function f2, which calls a function f3, which calls a function f4, which does visitation.
What we've been telling you from the start is that this forces you to partition your functions into two classes, one allowed to do visitation, the other not, then keep track of which is which, never calling the wrong class in a destructor, or from a catch clause.
And, of course, this has nothing to do with visitation, specifically. The exception safety guarantee is a global thing. Once you lose basic, you lose it everywhere, not just for visit.
I think I will take it to a separate thread in order not to obfuscate the review. One other remark I forgot to mention in the initial message is that the library upon assignment and emplacement prefers to first create a "temporary" object and then move from into the desired location. This works under the implicit assumption that the move constructor is cheaper that the said initialization. But I do not think it is the case in general. Moves are faster compared to other initialization only in the cases where the type is implemented as a handle to parts that are remote: allocated on heap, or stored somewhere in the kernel, and who often have weak invariants: in the moved from state they do not represent any resource. But other types like std::array<>, static_vector, even if they provide noexcept move are as expensive as making another copy. This is another price to be paid for the never-empty guarantee. There are also the cases where some type has move operations that can obviously never fail, but the author forgot to annotate them with noexcept (or noexcept was not available at the time when the type was designed). This last problem could be addressed if variant2 did not use std::is_nothrow_move_constructible directly, but a custom type trait that users would be able to specialize, in order to control the behavior of variant2. Regards, &rzej;
Andrzej Krzemienski wrote:
I have started a thread in this list a while ago, requesting for an example of code that *correctly handles exceptions* (does not stop stack unwinding at random places), where the programmer would make use of the never-empty guarantee , and chose something else than destroying or resetting the variant. And although I received some generic statements, referring to the purity of the design, strength of the invariants, and the easiness of thinking or "correctness", none of the proponents of the never-empty guarantee gave such an example.
template<class T> class monitor { private: char const* file_; int line_; T const& v_; T old_; public: explicit monitor( char const* file, int line, T const& v ): file_( file ), line_( line ), v_( v ), old_( v ) {} ~monitor() { if( v_ != old_ ) { std::clog << file_ << ":" << line_ << ": monitored value changed from " << old_ << " to " << v_ << std::endl; } } }; Or in general, in a destroy-only world, you can never read any values in a destructor or in a function called from a catch clause. It might be an interesting experiment to make Clang warn on such a use and then try it on some real codebases. These reads of a potentially-destroy-only values are invisible to you because we don't live in such a world.
wt., 2 kwi 2019 o 19:54 Peter Dimov via Boost
Andrzej Krzemienski wrote:
I have started a thread in this list a while ago, requesting for an example of code that *correctly handles exceptions* (does not stop stack unwinding at random places), where the programmer would make use of the never-empty guarantee , and chose something else than destroying or resetting the variant. And although I received some generic statements, referring to the purity of the design, strength of the invariants, and the easiness of thinking or "correctness", none of the proponents of the never-empty guarantee gave such an example.
template<class T> class monitor { private:
char const* file_; int line_; T const& v_; T old_;
public:
explicit monitor( char const* file, int line, T const& v ): file_( file ), line_( line ), v_( v ), old_( v ) {}
~monitor() { if( v_ != old_ ) { std::clog << file_ << ":" << line_ << ": monitored value changed from " << old_ << " to " << v_ << std::endl; } } };
Thank you. This is the best example I have seen so far.
Or in general, in a destroy-only world, you can never read any values in a destructor or in a function called from a catch clause.
Yes: this seems reasonable, you cannot just call any non-mutating function on a type you do nto know in a piece of the program that is expected not to fail (such as a destructor or scope guard). Similarly, you do not want to invoke arbitrary non-modifying functions in catch clauses on objects that may have suffered from a failure, whose type you do not know and whose invariants can potentially be very "weak". Logging in destructor is risky: you are doing more than cleanup and this can fail on its own. Your example would not compile because variant2 does not provide the streaming operator. Regards, &rzej; It might be an
interesting experiment to make Clang warn on such a use and then try it on some real codebases.
These reads of a potentially-destroy-only values are invisible to you because we don't live in such a world.
On 3/04/2019 05:27, Andrzej Krzemienski wrote:
This is not a full review yet. I just wanted to discuss the design first, especially the never-empty guarantee. And I am going to rehash the old argument.
The core of variant2 is its never-empty guarantee and the mechanism to achieve it: a number of different algorithms chosen depending on what the potentially stored types permit. In fact this mechanism is not much different than the one present in boost::variant.
I agree with most of the points that Andrzej has made, although I would go a step further and question why people want a never-empty variant in the first place. There is always potential uncertainty about the actual content type of a variant, requiring either assumptions (and UB or exceptions if wrong) or explicitly testing, eg. with visit. Checking if a variant is empty seems no different to me than checking if it contains an int; eliding either check confers exactly the same risks. Requiring that variants always possess an empty state dramatically simplifies implementation, including default construction and move assignment. And it avoids the valueless_by_exception kludge by promoting it to an entirely expected possible state of variant. (Thereby increasing the chance that consumers will consider it.) It also means that (like traditional monads), variant<T> is behaviorally equivalent (bar some naming conventions) to optional<T>, which seems like a consistent conceptual choice. And variant<> is valid and can only hold the empty state. Furthermore, while I can see possible value in a never-empty strong-guarantee variant (although I remain unconvinced that the "never-empty" part provides much benefit, as above); as soon as this guarantee is weakened then I don't see any advantage at all in trying to maintain a "never-empty" property. Unexpectedly changing type and/or content is exactly as bad as unexpectedly becoming empty.
On Tue, Apr 2, 2019 at 4:33 PM Gavin Lambert via Boost < boost@lists.boost.org> wrote:
There is always potential uncertainty about the actual content type of a variant, requiring either assumptions (and UB or exceptions if wrong) or explicitly testing, eg. with visit
I read the above as "UB or error handling if wrong". It does not matter what form of error handling is used. The use of "assumption" is imprecise, because the assumptions we're talking about are a matter of a conscious design choice. If we're assuming that the basic invariants are in place during error recovery (the basic guarantee), it is because we are writing code to ensure that it is true. Of course the point of it is to avoid having to explicitly test. The problem with explicitly testing is that it must be done everywhere because we don't know if maybe we are being called from an error-handling context (note, this doesn't necessarily mean exception handling). For example, every member function must check if the class invariants are in place for *this (assuming UB is not acceptable).
Checking if a variant is empty seems no different to me than checking if it contains an int; eliding either check confers exactly the same risks.
Under the basic guarantee, you'd only check if it contains an int if you care whether it contains an int. Typical use is not to check, but to do something if it contains an int and something else if it contains a different type. There is no check.
Unexpectedly changing type and/or content is exactly as bad as unexpectedly becoming empty.
It is not as bad. Introducing the empty state forces us to define behavior for that state. This is logically equivalent to requiring well defined behavior from any member function even for instances that failed to initialize. It's better to ensure and then assume that this can not happen.
On 3/04/2019 13:38, Emil Dotchevski wrote:
The problem with explicitly testing is that it must be done everywhere because we don't know if maybe we are being called from an error-handling context (note, this doesn't necessarily mean exception handling). For example, every member function must check if the class invariants are in place for *this (assuming UB is not acceptable).
How is this different if the instance can be empty, or can unexpectedly change from type A to type B? If you don't know whether you're handling an errored instance or not, then it could be unexpectedly empty (with one variant implementation) or it could be unexpectedly type B (with another implementation). I don't see any practical difference between these cases.
Under the basic guarantee, you'd only check if it contains an int if you care whether it contains an int. Typical use is not to check, but to do something if it contains an int and something else if it contains a different type. There is no check.
That's still a check, even if the check is inside of visit().
It is not as bad. Introducing the empty state forces us to define behavior for that state. This is logically equivalent to requiring well defined behavior from any member function even for instances that failed to initialize. It's better to ensure and then assume that this can not happen.
The empty state is just another type that the variant can hold. Think
of it as std::monostate always implicitly being the first type argument
of the variant (as Peter points out).
My point is that I don't see any advantages to requiring that people
specify this explicitly, and I only see disadvantages to allowing people
to not specify it at all.
If it's illegal for the first type of the variant to be anything other
than std::monostate (which I think should be the case), then it could be
implicit rather than explicit. And both implementation and usage of the
variant would be dramatically simplified.
And then you also get the useful properties that variant<> is valid
(which is handy for generic variadic code) and variant<T> is equivalent
to optional<T> (and variant
On Tue, Apr 2, 2019 at 7:07 PM Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 3/04/2019 13:38, Emil Dotchevski wrote:
The problem with explicitly testing is that it must be done everywhere because we don't know if maybe we are being called from an
error-handling
context (note, this doesn't necessarily mean exception handling). For example, every member function must check if the class invariants are in place for *this (assuming UB is not acceptable).
How is this different if the instance can be empty, or can unexpectedly change from type A to type B?
If you don't know whether you're handling an errored instance or not, then it could be unexpectedly empty (with one variant implementation) or it could be unexpectedly type B (with another implementation). I don't see any practical difference between these cases.
Consistent with the basic guarantee, if you assign one std::vector to another and the copy operation fails, the contents of the target vector is not specified, but it is guaranteed to be a valid vector, so that if you happen to handle it later, you can safely do with it what you do with any other vector: you can insert or delete elements, iterate, whatever. By analogy with your position, a better design for std::vector would be to define a special "error" state that you must check for *every* time you handle a vector, and the justification for that design is that otherwise it might be unexpected to see a vector in a half-copied (but valid, by definition) state. Do you see the practical difference now? Going back to variant, in one case, we are defining an empty state, which forces the user to provide explicit handling for that state, *everywhere*. Otherwise, we are not defining an empty state, and the user doesn't have to provide special handling, *anywhere*, because he doesn't have to care that A "unexpectedly" changed into B. He sees a B, he does what he normally does with B.
On 4/04/2019 13:26, Emil Dotchevski wrote:
Consistent with the basic guarantee, if you assign one std::vector to another and the copy operation fails, the contents of the target vector is not specified, but it is guaranteed to be a valid vector, so that if you happen to handle it later, you can safely do with it what you do with any other vector: you can insert or delete elements, iterate, whatever.
By analogy with your position, a better design for std::vector would be to define a special "error" state that you must check for *every* time you handle a vector, and the justification for that design is that otherwise it might be unexpected to see a vector in a half-copied (but valid, by definition) state. Do you see the practical difference now?
No, because vector already has that state -- it's called "a vector with size zero" (aka "empty"), which is the same state it typically transitions to when successfully moved-from. This is also the case for all the smart pointer types. I am saying that variant should behave exactly the same way. It's what people expect.
On Thu, Apr 4, 2019 at 3:14 PM Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 4/04/2019 13:26, Emil Dotchevski wrote:
Consistent with the basic guarantee, if you assign one std::vector to another and the copy operation fails, the contents of the target vector
is
not specified, but it is guaranteed to be a valid vector, so that if you happen to handle it later, you can safely do with it what you do with any other vector: you can insert or delete elements, iterate, whatever.
By analogy with your position, a better design for std::vector would be to define a special "error" state that you must check for *every* time you handle a vector, and the justification for that design is that otherwise it might be unexpected to see a vector in a half-copied (but valid, by definition) state. Do you see the practical difference now?
No, because vector already has that state -- it's called "a vector with size zero" (aka "empty"), which is the same state it typically transitions to when successfully moved-from.
template <class T> void f( std::vector<T> & x, std::vector<T> const & y ) { x = y; } Above, if the assignment fails, the content of x is unspecified, but it is guaranteed to be a valid vector, containing valid objects (if not empty). By analogy: if a variant2 assignment fails, its value is unspecified, but it is guaranteed to be valid.
Gavin Lambert wrote:
Requiring that variants always possess an empty state dramatically simplifies implementation, including default construction and move assignment. And it avoids the valueless_by_exception kludge by promoting it to an entirely expected possible state of variant. (Thereby increasing the chance that consumers will consider it.)
That's more or less what using `monostate` as the first alternative achieves.
śr., 3 kwi 2019 o 01:33 Gavin Lambert via Boost
On 3/04/2019 05:27, Andrzej Krzemienski wrote:
This is not a full review yet. I just wanted to discuss the design first, especially the never-empty guarantee. And I am going to rehash the old argument.
The core of variant2 is its never-empty guarantee and the mechanism to achieve it: a number of different algorithms chosen depending on what the potentially stored types permit. In fact this mechanism is not much different than the one present in boost::variant.
I agree with most of the points that Andrzej has made, although I would go a step further and question why people want a never-empty variant in the first place.
There is always potential uncertainty about the actual content type of a variant, requiring either assumptions (and UB or exceptions if wrong) or explicitly testing, eg. with visit. Checking if a variant is empty seems no different to me than checking if it contains an int; eliding either check confers exactly the same risks.
Requiring that variants always possess an empty state dramatically simplifies implementation, including default construction and move assignment. And it avoids the valueless_by_exception kludge by promoting it to an entirely expected possible state of variant. (Thereby increasing the chance that consumers will consider it.)
It also means that (like traditional monads), variant<T> is behaviorally equivalent (bar some naming conventions) to optional<T>, which seems like a consistent conceptual choice. And variant<> is valid and can only hold the empty state.
In principle, the stronger the invariant, the fewer opportunities to make bug. The strive for never-empty variant is conceptually similar to a strive for a pointer that does not have the null-pointer state. We often give advice to people that as long as this is possible you should prefer using references to using pointers, because the former do not have the null state. The significant portion of crashes in the programs I have worked with was when a null pointer was dereferenced. Function f() returns a shared_ptr<> and the implied contract is, it is not null. I do not want to check for null pointer whenever I use it. But at some point some clever developer comes to an idea that if he doesn't know what to do, he will return a null pointer. These problems would not happen if there was a type never_null_shared_ptr. It just wipes a class of bugs from existence. I am myself in favor of never-empty guarantee for variant, as long as it does not cost too much. The notion of how easy/hard it is to implement something should not drive the interface and the contract of a type. I object to the never-empty guarantee for variant because it costs too much and affects the contract of the type. In C++ performance characteristics of a type are part of its contract. Regards, &rzej;
Furthermore, while I can see possible value in a never-empty strong-guarantee variant (although I remain unconvinced that the "never-empty" part provides much benefit, as above); as soon as this guarantee is weakened then I don't see any advantage at all in trying to maintain a "never-empty" property. Unexpectedly changing type and/or content is exactly as bad as unexpectedly becoming empty.
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
I have to say that I'm with Andrzej on this matter. Mabye I'm blind, but it seems to me that people repeatedly mix up "never empty" and "no error state". Variant2 can still get into an error state, just that that error state is not explicitly named and modeled as part of variant2, but instead, a "regular" state is reused to signal that error: Either monostate or whatever is the first type that has a non throwing default constructor. It is only if neither of those options is available that variant2 falls back to double buffering and does actually avoid ending up in an error state at all. Also, I don't see the problem with valueless_by_exception. Other than nullptr, you don't have to explicitly check for it most of the time (at least not more often than you'd have to check for monostate in variant2). Consider what you can do with a variant: - get: If you are guessing the current type wrong, you get an exception Doesn't matter if the actual type is valueless_by_exception or just an unexpected regular type. - get_if: Again, if you are guessing wrong, you get a nullptr - copy/move: valueless again behaves just like another type - index: Valueless just returns a unique index. The only special thing about it is that it doesn't fall into the regular range between zero and the number of member types. - comparison operators: valueless_by_exception is even more convenient, because you don't have to check first if the left and right hand side are of the same type, but you can still treat it just as any other T if you want. - visit: This is the only case where valueless is really special: You get an exception instead of having to provide an overload for e.g. monostate. Considering how rarely a variant ends up in such a state, throwing an exception is imho the right thing to do most of the time anyway (use an exception to report exceptional errors). But when that is not appropriate, then yes, you have to either check beforehand or catch the exception instead of building the logic into your visitor. Of course I can imagine a scenario, where one or the other approach is simpler, but 99% of the time it really doesn't make a big difference. What I'm trying to say here is not that the design of std::variant is better than boost::variant2, but that they are just two different design choices that may be more or less convenient in a particular scenario, but overall don't make a big difference one way or the other. What I personally do find a bit problematic is that variant2 would be yet another variant type that is not completely compatible to std::variant, without providing significant benefits or a fundamentally different design (e.g. providing strong exception guarantee or dramatically improved compile-time or run-time performance). Mike Dev
On Wed, Apr 3, 2019 at 2:08 AM Mike via Boost
Mabye I'm blind, but it seems to me that people repeatedly mix up "never empty" and "no error state".
Variant2 can still get into an error state, just that that error state is not explicitly named and modeled as part of variant2, but instead, a "regular" state is reused to signal that error:
What you're missing is that this is a matter of definition. Your definition is: if we enter a state because of an error, then that state is an error state. An alternative definition is: if an assignment operation fails, the object is left in an unspecified but valid state. Under the second definition, it is illogical to call the "unspecified but valid" state an error state that must be checked, because by definition it is a perfectly valid state. Further, this valid state is not "reused to signal that error". It is the result of the error, but the error is communicated by other means, e.g. an exception.
participants (5)
-
Andrzej Krzemienski
-
Emil Dotchevski
-
Gavin Lambert
-
Mike
-
Peter Dimov