On 05 Jun 2015, at 10:17 , Markus Grech <markus...@gmail.com> wrote:Hi everyone,I would like to voice my concerns about default construction of std::variant and std::monostate. The proposal did not make clear to me why default constructed empty state is such a big issue in the first place and the current alternative suffers from significant drawbacks:
- To me it is very surprising that variant tries to default-construct the first type. Why not the 2nd, 5th type? I'd rather have variant have no default constructor than some arbitrary decision.
- It is yet another special case that users need to be taught about and frankly, it's not pretty either. Users have to remember "Oh, I have to use this std::monostate workaround if my type is not default constructible!". Hacks (another spelling for 'workaround') are bad.
- It does not play well with changes. Imagine if the user-defined type that was previously default constructible has its default constructor removed. Now the users needs to add std::monostate all over the place and fix up all the indices for index-based access.
There needs to be a better solution. I do think that having an empty state semantically fails at the idea of variant in the first place ("either A or B", not "either A, B or empty"), but it is not the end of the world. The current std::monostate design however has significant issues that IMHO deserve another look.
Hi everyone,I would like to voice my concerns about default construction of std::variant and std::monostate. The proposal did not make clear to me why default constructed empty state is such a big issue in the first place and the current alternative suffers from significant drawbacks:
- To me it is very surprising that variant tries to default-construct the first type. Why not the 2nd, 5th type? I'd rather have variant have no default constructor than some arbitrary decision.
- It is yet another special case that users need to be taught about and frankly, it's not pretty either. Users have to remember "Oh, I have to use this std::monostate workaround if my type is not default constructible!". Hacks (another spelling for 'workaround') are bad.
- It does not play well with changes. Imagine if the user-defined type that was previously default constructible has its default constructor removed. Now the users needs to add std::monostate all over the place and fix up all the indices for index-based access.
I do think that having an empty state semantically fails at the idea of variant in the first place ("either A or B", not "either A, B or empty"), but it is not the end of the world. The current std::monostate design however has significant issues that IMHO deserve another look.
What about the new extended union(i.e don`t generate special members if any field lacks)?The last version of boost::variant that I am aware of calls for thoughts on having a blank state.
So what is wrong with having a null state?'
I personally prefer to implement std::optional<T> in terms of std::variant<T>. I also think that std::function can be defined in terms of a null-able std::variant type.
... why? It's highly unlikely that a person who uses `optional` will suddenly change it to `variant` and want all his code to work without changes. It's also unlikely that template code that accepts `optional<T>` would be able to reasonably work with `variant<T...>`.
I'm being at least partially serious here. If you would prefer not to rely on the default constructor's behavior, then just don't use it. Sure, the compiler won't error if you use it by accident, which is unfortunate. But you are free to avoid the constructor if you so desire.
But we have to live within the limitations of the language that exists, not the language we would like to use.
On Saturday, June 6, 2015 at 10:12:33 AM UTC-7, Nicol Bolas wrote:
... why? It's highly unlikely that a person who uses `optional` will suddenly change it to `variant` and want all his code to work without changes. It's also unlikely that template code that accepts `optional<T>` would be able to reasonably work with `variant<T...>`.Why reimplement a ton of machinery you already have?
Why make users include two big headers with a bunch similar templates if they happen to use both library facilities?
Learning not to needlessly redesign the wheel is one of the first instincts we try to instill in first-year engineering students.
Of course, as we've already discussed, both variant and optional and expected are just over-complicated templates that poorly emulate small subsets of decades-old type theory research that could just be built-in to the language. :p
I'm being at least partially serious here. If you would prefer not to rely on the default constructor's behavior, then just don't use it. Sure, the compiler won't error if you use it by accident, which is unfortunate. But you are free to avoid the constructor if you so desire.Ah, yes. It's so very very easy to make sure your generic code isn't accidentally default constructing or copying something behind your back. I've certainly never spent hours tracking down one-character mistakes in container libraries that could have been easily tracked down had the contained types' constructors just been deleted.And certainly there are no libraries that usefully change behavior after detecting if a type is default constructible or not.
But we have to live within the limitations of the language that exists, not the language we would like to use.... we are discussing this on a mailing that is specifically about changing the limitations of the language that exists and making the language into the one we would like to use.
Le 05/06/15 10:17, Markus Grech a écrit :
We have two orthogonal interface decissions that could interactHi everyone,
I would like to voice my concerns about default construction of std::variant and std::monostate. The proposal did not make clear to me why default constructed empty state is such a big issue in the first place and the current alternative suffers from significant drawbacks:
- To me it is very surprising that variant tries to default-construct the first type. Why not the 2nd, 5th type? I'd rather have variant have no default constructor than some arbitrary decision.
- It is yet another special case that users need to be taught about and frankly, it's not pretty either. Users have to remember "Oh, I have to use this std::monostate workaround if my type is not default constructible!". Hacks (another spelling for 'workaround') are bad.
- It does not play well with changes. Imagine if the user-defined type that was previously default constructible has its default constructor removed. Now the users needs to add std::monostate all over the place and fix up all the indices for index-based access.
There needs to be a better solution. I do think that having an empty state semantically fails at the idea of variant in the first place ("either A or B", not "either A, B or empty"), but it is not the end of the world. The current std::monostate design however has significant issues that IMHO deserve another look.
* default construction
* possible empty
A - We can have a variant class that is not default constructible and cannot be empty.
B - We can have a variant class that is default constructible and can be empty, so the default constructor is naturally empty.
C - We can have a variant class that is default constructible and cannot be empty (we need to state how it is default constructed).
The not default constructible and empty seems not interesting enough.
The current proposal correspond to C, when its first alternative is default constructible and A otherwise, and I think this is the better we can have.
The standard could also have a class behaving always as A. I wouldn't be against.
So we have a very rare state of a should-be-rare type. Let's not litter our code with if( !v.empty() ).... for that rare case. And if you do get that rare case, you probably handled it via throw/catch, so your variant-reading code doesn't happen, so again, you don't need to check for empty.
So I don't think the empty state is worth it for throwing moves.
--
On 2015–09–21, at 4:56 PM, Michael Park <mcy...@gmail.com> wrote:Hello, I've been working on an alternative variant proposal. It's still very rough at this point but I've captured the big questions I wanted to answer, and would appreciate feedback from the community.The following are some of the principles the design is based on:(1) union is not a good starting point for a discriminated union. I believe better starting points are:
From C++: enum as a special case where each member is a unit type, andclass inheritance where an abstract base class is a discriminated union of its derived classes.From other languages: Sum types from Haskell and ML or enum from Rust.
(2) The order of the types specified in variant<Ts...> should not change its behavior.
(3) The API should be minimal, useful, and consistent.The following are few design decisions that fall out from the above principles:* The visitation interface is a type_switch expression which looks similar to a regular switch statement,as well as match expressions from functional languages. (1)* Default construction should not construct the first type. (2)* The members should be discriminated by type rather than the index. (2), (3)(i.e. variant<int, string, int> behaves equivalently to variant<int, string>)* There is no special treatment for the null state. (3)
On 2015–09–21, at 4:56 PM, Michael Park <mcy...@gmail.com> wrote:Hello, I've been working on an alternative variant proposal. It's still very rough at this point but I've captured the big questions I wanted to answer, and would appreciate feedback from the community.The following are some of the principles the design is based on:(1) union is not a good starting point for a discriminated union. I believe better starting points are:union is the only methodology with a chance at constexpr compatibility.
From C++: enum as a special case where each member is a unit type, andclass inheritance where an abstract base class is a discriminated union of its derived classes.From other languages: Sum types from Haskell and ML or enum from Rust.Wait, what do you mean by “starting point”? How is any C++ variant template still like a union, to the user?
(2) The order of the types specified in variant<Ts...> should not change its behavior.Agreed. The better evolutionary direction is to allow the implementation to sort the type-list, so any permutation of the same type-list names the same variant type.It’s only a series of unfortunate events that led std::type_info (and its member less()) to be non-constexpr, preventing this normalization from being done already.
(3) The API should be minimal, useful, and consistent.The following are few design decisions that fall out from the above principles:* The visitation interface is a type_switch expression which looks similar to a regular switch statement,as well as match expressions from functional languages. (1)* Default construction should not construct the first type. (2)* The members should be discriminated by type rather than the index. (2), (3)(i.e. variant<int, string, int> behaves equivalently to variant<int, string>)* There is no special treatment for the null state. (3)What the heck does this mean? The question is whether a null state exists or not. How can it exist but not be special?
I think, given the controversy, nullable and non-nullable versions should be supported. For example, it’s nullable if void appears in the typelist. Let nullable_variant (not actual proposed name) be an alias template to nonnull_variant<void, T ...>.
This is the work-in-progress in a Google Doc: Variant and it is open for comments.The #1 problem with N4542, IMHO, is that it aggregates and idealizes some existing implementations without actually prototyping the result.
I can’t speak for everyone, but I’d prefer to see a paper laying out arguments and principles for their own sake, not a complete self-contained proposal going from ancient history up to standardese.
On 2015–09–21, at 4:56 PM, Michael Park <mcy...@gmail.com> wrote:(2) The order of the types specified in variant<Ts...> should not change its behavior.Agreed. The better evolutionary direction is to allow the implementation to sort the type-list, so any permutation of the same type-list names the same variant type.
It’s only a series of unfortunate events that led std::type_info (and its member less()) to be non-constexpr, preventing this normalization from being done already.(3) The API should be minimal, useful, and consistent.The following are few design decisions that fall out from the above principles:* The visitation interface is a type_switch expression which looks similar to a regular switch statement,as well as match expressions from functional languages. (1)* Default construction should not construct the first type. (2)* The members should be discriminated by type rather than the index. (2), (3)(i.e. variant<int, string, int> behaves equivalently to variant<int, string>)* There is no special treatment for the null state. (3)What the heck does this mean? The question is whether a null state exists or not. How can it exist but not be special?I think, given the controversy, nullable and non-nullable versions should be supported. For example, it’s nullable if void appears in the typelist. Let nullable_variant (not actual proposed name) be an alias template to nonnull_variant<void, T ...>.
This is the work-in-progress in a Google Doc: Variant and it is open for comments.The #1 problem with N4542, IMHO, is that it aggregates and idealizes some existing implementations without actually prototyping the result.
I can’t speak for everyone, but I’d prefer to see a paper laying out arguments and principles for their own sake, not a complete self-contained proposal going from ancient history up to standardese.
Personally, I don't see why it's so important to have a few default behaviors based on the ordering in the list.
Personally, I don't see what's wrong with having a few default behaviors based on the ordering in the list.
On 2015–09–21, at 6:59 PM, Michael Park <mcy...@gmail.com> wrote:I believe the variant proposed in N4542 is like a union to the user in the following ways:
- Default construction tries to default construct the first type.
- The alternatives are discriminated by the index, rather than the type. This leads to the result that variant<int, int> carries 2 distinct states of int,
which means that index-based operations such as the index-based in-place constructor, and index-based get must be provided.
Sorry that I was unclear about this. What I mean here is that a null state exists by the presence of the null state tag, null_t,but it's not special in the sense that variant does not change behavior in its presence. It treats null_t as it would any other type.It does not do any of the following:
- enable the default constructor only if variant is nullable
- set the variant to the null state rather than the valid, unspecified state on assignment failure if variant is nullable
- provide a operator bool() if variant is nullable
This is the work-in-progress in a Google Doc: Variant and it is open for comments.The #1 problem with N4542, IMHO, is that it aggregates and idealizes some existing implementations without actually prototyping the result.I have a working implementation to supplement this proposal, but I haven't mentioned it because it's a bit out of date.It currently reflects the last iteration of the library. I'll be updating the implementation throughout this week, but currently you can take a look at https://github.com/mpark/variant
I can’t speak for everyone, but I’d prefer to see a paper laying out arguments and principles for their own sake, not a complete self-contained proposal going from ancient history up to standardese.Could you please elaborate a little on what you mean by "laying out arguments and principles for their own sake”?
On 2015–09–21, at 8:18 PM, David Krauss <pot...@mac.com> wrote:Agreed; variant assignment failure should work the same as any, function, array, or any non-node-based handle class. I’m surprised this is even being considered as a problem in particular.
On 2015–09–21, at 6:59 PM, Michael Park <mcy...@gmail.com> wrote:
- The alternatives are discriminated by the index, rather than the type. This leads to the result that variant<int, int> carries 2 distinct states of int,
which means that index-based operations such as the index-based in-place constructor, and index-based get must be provided.They’re discriminated by either, but as mentioned, I don’t think numeric indexing is a good idea. (I guess you mean that unions can have different-named members of the same type, and numbers are like names.)
I’m not going back just now to review N4542, but I thought I saw it forbid including the same type twice.
Such a violation of a library precondition would result in UB.
Sorry that I was unclear about this. What I mean here is that a null state exists by the presence of the null state tag, null_t,but it's not special in the sense that variant does not change behavior in its presence. It treats null_t as it would any other type.It does not do any of the following:Why not spell null_t as void?
null_t sounds like a type with a single value, perhaps one which can be compared to a variant or passed as a constructor overloading dispatch tag.
Any type, even such a special wacky one, should be supported within variant. (Which motivates avoidance of having special singletons in the first place.)
The type with no values is void.
On 2015–09–21, at 8:15 PM, Nicol Bolas <jmck...@gmail.com> wrote:Sort it based on what, exactly?
Personally, I don't see why it's so important to have a few default behaviors based on the ordering in the list. If you're going to have default construction that constructs one element, then the user should be able to control what that default element is. Who cares what other languages do; this is C++, and C++ has its own unique needs.
If you don't want behavior based on the order of items in the list, fine; don't use those behaviors. Don't default-construct your variants.
I don't think that `void` would be appropriate for the nullable type. While you can get pointers to void, you can't get void references. It just doesn't behave like a normal type. It would be better to define an explicit, empty type that would be treated by users as the empty type for variants.
Also, I really prefer the current design, where there is a distinction between being empty and being in a state that the user considers empty. The former can only happen as a consequence of a copy/move failure; it exists solely for error handling as an unfortunate consequence of the C++ language.
This is the work-in-progress in a Google Doc: Variant and it is open for comments.The #1 problem with N4542, IMHO, is that it aggregates and idealizes some existing implementations without actually prototyping the result.
To be fair, there's not much difference between Boost.Variant and N4542. The principle differences are the empty-as-error state and C++11/14 features. In principle, the visitation has been around for quite some time and has a lot of user-experience behind it.
And it's not like std::any and std::optional didn't do more or less the same thing. Or std::shared_ptr, for that matter.
It's a Boost component being introduced to the standard library.I can’t speak for everyone, but I’d prefer to see a paper laying out arguments and principles for their own sake, not a complete self-contained proposal going from ancient history up to standardese.
That would make things take longer.
On 2015–09–21, at 8:40 PM, Nicol Bolas <jmck...@gmail.com> wrote:Personally, I don't like numeric indexing either, but I also never understood why the committee allowed numeric indexing of tuples. The way I see it, they're equally wrong, but if we're going to have one, we should have the other for orthogonality's sake.
Actually, it explicitly allows it; multiple repeated types are distinct states. But it says that, for such a template, using a type-based getter always fails; you have to access the element based on the index. Same goes for emplacement construction.
A variant in the "null" state has a value, just like a pointer in the "null" state has a value. So even conceptually, `void` is the wrong thing.
`void` is not something we should try to use. It's something we should try to avoid using.
Hello, I've been working on an alternative variant proposal. It's still very rough at this point but I've captured the big questions I wanted to answer, and would appreciate feedback from the community.
The following are some of the principles the design is based on:(1) union is not a good starting point for a discriminated union. I believe better starting points are:From C++: enum as a special case where each member is a unit type, andclass inheritance where an abstract base class is a discriminated union of its derived classes.From other languages: Sum types from Haskell and ML or enum from Rust.
(2) The order of the types specified in variant<Ts...> should not change its behavior.
* Default construction should not construct the first type. (2)
* There is no special treatment for the null state. (3)
On 2015–09–21, at 8:40 PM, Nicol Bolas <jmck...@gmail.com> wrote:Personally, I don't like numeric indexing either, but I also never understood why the committee allowed numeric indexing of tuples. The way I see it, they're equally wrong, but if we're going to have one, we should have the other for orthogonality's sake.A tuple is an ordered sequence of values. It cannot be discriminated by types, because repetition of a type isn’t special. (Such discrimination has now been added, but it only works for special cases.)A variant has a value of one of several types. As Michael noted, indexing the types fundamentally changes their meaning. Now you don’t have a variant of one of several types, but one of several type-index pairs. The amount of information in the variant value has been measurably increased.
Actually, it explicitly allows it; multiple repeated types are distinct states. But it says that, for such a template, using a type-based getter always fails; you have to access the element based on the index. Same goes for emplacement construction.Oh, that’s broken. I don’t want a template that breaks with repeated types. Perhaps I want one template that works with repeated types, and another that diagnoses them, and allows type-based discrimination. But repeated types don’t sound too appetizing at all, especially if they’re not simply deduplicated.
A variant in the "null" state has a value, just like a pointer in the "null" state has a value. So even conceptually, `void` is the wrong thing.The variant has a value, but it does not hold a value. A variant in the special error state you mentioned also has a value.
`void` is not something we should try to use. It's something we should try to avoid using.That doesn’t make sense. It’s not an anti-pattern; it’s a pure and valid concept. It represents something you can’t use. It represents “nothing there.”
struct CopyVisitor
{
template<T> auto operator()(const T &t) const {return t;}
};
struct CopyVisitorAndVoid
{
template<T> auto operator()(const T &t) const {return t;}
void operator()() {return;}
};
On 2015–09–21, at 8:15 PM, Nicol Bolas <jmck...@gmail.com> wrote:Sort it based on what, exactly?Implementation-defined. Probably, the collation of mangled type-names.
Personally, I don't see why it's so important to have a few default behaviors based on the ordering in the list. If you're going to have default construction that constructs one element, then the user should be able to control what that default element is. Who cares what other languages do; this is C++, and C++ has its own unique needs.Sure, but specifying that by the order of the list is a bit impure. For tagging types, we have tag types.
I don't think that `void` would be appropriate for the nullable type. While you can get pointers to void, you can't get void references. It just doesn't behave like a normal type. It would be better to define an explicit, empty type that would be treated by users as the empty type for variants.No references is exactly the point. Why should the user be able to get a reference the the nonexistent content of a disengaged variant?
Also, I really prefer the current design, where there is a distinction between being empty and being in a state that the user considers empty. The former can only happen as a consequence of a copy/move failure; it exists solely for error handling as an unfortunate consequence of the C++ language.I’ve not been involved in the variant debates, but this sounds crazy. I assume operator bool maps both empty states to false?
This is the work-in-progress in a Google Doc: Variant and it is open for comments.The #1 problem with N4542, IMHO, is that it aggregates and idealizes some existing implementations without actually prototyping the result.
To be fair, there's not much difference between Boost.Variant and N4542. The principle differences are the empty-as-error state and C++11/14 features. In principle, the visitation has been around for quite some time and has a lot of user-experience behind it.
And it's not like std::any and std::optional didn't do more or less the same thing. Or std::shared_ptr, for that matter.That doesn’t mean Boost always gets a free pass, or that those classes are out of the woods. (You mean experimental::any and experimental::optional.)
It's a Boost component being introduced to the standard library.I can’t speak for everyone, but I’d prefer to see a paper laying out arguments and principles for their own sake, not a complete self-contained proposal going from ancient history up to standardese.
That would make things take longer.One section of N4542, which is the fourth revision in its sequence, is a record of straw poll votes being taken after the third revision. I think the committee should decide on design principles, given solid proposals and argumentation, and then apply those principles to the various new erasure classes to promote their interoperability.
Iterating on big proposals full of little details doesn’t seem to be working.
Hello, I've been working on an alternative variant proposal. It's still very rough at this point but I've captured the big questions I wanted to answer, and would appreciate feedback from the community.The following are some of the principles the design is based on:(1) union is not a good starting point for a discriminated union. I believe better starting points are:From C++: enum as a special case where each member is a unit type, andclass inheritance where an abstract base class is a discriminated union of its derived classes.From other languages: Sum types from Haskell and ML or enum from Rust.(2) The order of the types specified in variant<Ts...> should not change its behavior.(3) The API should be minimal, useful, and consistent.The following are few design decisions that fall out from the above principles:* The visitation interface is a type_switch expression which looks similar to a regular switch statement,as well as match expressions from functional languages. (1)* Default construction should not construct the first type. (2)* The members should be discriminated by type rather than the index. (2), (3)(i.e. variant<int, string, int> behaves equivalently to variant<int, string>)* There is no special treatment for the null state. (3)This is the work-in-progress in a Google Doc: Variant and it is open for comments.I'm in Seattle attending CppCon this week. Please reach out if you're also here and would like to provide feedback and/or have further design discussions!Thanks,MPark.
On 2015–09–22, at 1:06 AM, Nicol Bolas <jmck...@gmail.com> wrote:Conceptually, perhaps. But not in any way that actually takes up storage or anything. So it doesn't impact the quality of the variant's implementation.
And if someone can find a use for it, more power to them. Especially if it allows them to put different typedefs in the same variant.
The only thing it does for the implementation is prevent it from re-ordering the elements in the variant.
We should not create an arbitrary restriction on a C++ type, or choose not to implement genuinely useful functionality, based on some external concept of what a `variant` ought to be.
Well, if duplicate types are "deduplicated", then what's the point in allowing them at all? If you're not going to allow duplicate types to be considered separate states, then there's no point in allowing them at all.
But the variant does hold a value. It holds a value that means "nothing meaningful." That value, like "null" for pointers, is a value. It has all of the rights, powers, and functions of a real C++ value. You can create them, copy them, pass them around, and store them.
You can't do any of that with `void`.
I can create a value of type `nullptr_t`. I can create a value of type `nullopt_t`. I can create references to both of those.
I cannot create a value of type `void`. It is not a valid C++ type. It lacks orthogonality with the behavior of C++ types. That lack of orthogonality is what makes using it like this an anti-pattern.
Consider a simple copy visitor class for a variant. All it does is return a copy of the value:struct CopyVisitor
{
template<T> auto operator()(const T &t) const {return t;}
};
Well, you can't actually do that with `void`, can you? You can't get a reference to `void`, and you can't return a copy of `void`. So you would need to create a special overload solely to handle this case:
Template code has to deal with this kind of "what if T is void" question a lot. The best answer: stop using `void`.
On 2015–09–22, at 1:30 AM, Nicol Bolas <jmck...@gmail.com> wrote:If it's implementation-dependent... what does it matter if it sorts them or not?
I think you're trying to get `variant<int, float>` and `variant<float, int>` to be the same. Well, C++ doesn't allow that; different template instantiations will be different types, period.
I don't know what you mean by "tag types". If you're talking about things like iterator categories, those are way too intrusive to use, and can only be used by certain special functions.
Also, what does it matter if it is "impure"? How do you even define "purity" here?
Orthogonality. I covered why this is important in a previous post.
The current proposal only has one "empty" state, and it can only be caused by copy/move failure. The user can decide if a particular state conceptually represents "being empty", but that's up to the user. The current proposal does not provide some specific type that, when put into the type list, is universally considered being empty.
The standards committee likes to standardizes existing (C++) practice. Boost.Any, Boost.Optional, and Boost.Variant are all existing practice.
What you're talking about is not.
That's one way to look at it. Another way to look at it is to recognize that the "various new erausre classes" are not all "erasure classes". Neither `variant` nor `optional` is specifically about type erasure, so your nomenclature is debatable. They represent distinct concepts with minimally-overlapping use cases, and thus they do not strictly need to have a unified interface.
Iterating on big proposals full of little details doesn’t seem to be working.
Everyone judges "working" based on whether it leads to the result they want.
We have `any` and `optional` in TS's. Given the construction and reception of N4542, we will likely have `variant`, as well as probably all three in C++17. These classes will fill good and useful niches in the C++ language.
That's the only objective definition of "working". So objectively speaking, the process seems to be working.
On 2015–09–22, at 8:42 AM, Agustín K-ballo Bergé <kaba...@hotmail.com> wrote:I'm not sure if I'm readying this correctly, in Eggs.Variant duplicated types are distinct members. Maybe that's what you mean by "no distinction of duplicates", maybe you mean that duplicates are not distinct, I don't know... So to be explicit:
T
shall occur exactly once in Ts...” and jumped the gun.
I've found this with raw `union`s as both distinct members with the same type, as well as a single member that changes meaning based on the value of the discriminator (yuck). This was also raised as to support generic programing, the discriminator of a discriminated union is meaningful and contributes to its value.
For these reason, functionality is presented both as type based (for when you absolutely know the types you are dealing with and they are guaranteed to be distinct, like `std::string` and `std::vector`) as well as index based (for when you have duplicated distinct types or you just don't know, aliases, generic contexts, etc).
That said, I'd rather use names (meaning!) than either indices or types, raw `union`s still beat my solution there. Using named constants for indices helps some, but introduces another point where things can possiblie go wrong. There's some work going on on tagged types that, if transparently supported, could be promising to close the gap.
On Mon, Sep 21, 2015 at 1:56 AM, Michael Park <mcy...@gmail.com> wrote:Hello, I've been working on an alternative variant proposal. It's still very rough at this point but I've captured the big questions I wanted to answer, and would appreciate feedback from the community.This is a bit divorced from the actual implementation issues. I'm also not sure I follow the purpose of the relationship drawn to pointers/inheritance in the proposal. I recommend cutting this. A variant is just a sum type and deals with a closed set of types. Relating to inheritance is unnecessary and confusing, and arguably inaccurate.
On Mon, Sep 21, 2015 at 1:56 AM, Michael Park <mcy...@gmail.com> wrote:The following are some of the principles the design is based on:(1) union is not a good starting point for a discriminated union. I believe better starting points are:From C++: enum as a special case where each member is a unit type, andclass inheritance where an abstract base class is a discriminated union of its derived classes.From other languages: Sum types from Haskell and ML or enum from Rust.It needs to be a union for minimal constexpr-ness.
On Mon, Sep 21, 2015 at 1:56 AM, Michael Park <mcy...@gmail.com> wrote:(2) The order of the types specified in variant<Ts...> should not change its behavior.Why? Ordering actually ends up being important in usage, and so does indexing by value. Without that, it's really difficult to do basic things, such as portable serialization. If users want some kind of common ordering for some reason, then they can wrap it themselves. Similarly, if implementations can do that internally based on intrinsics or something to reduce instantiations, they can, but at the top level they need some kind of consistent and portable discriminator, otherwise you are missing some important use cases.
On Mon, Sep 21, 2015 at 1:56 AM, Michael Park <mcy...@gmail.com> wrote:* Default construction should not construct the first type. (2)Why? If there there is default construction at all, it should be consistent across library implementations for portability, and the rules should be simple.
On Monday, September 21, 2015 at 8:51:04 AM UTC-4, David Krauss wrote:On 2015–09–21, at 8:40 PM, Nicol Bolas <jmck...@gmail.com> wrote:Personally, I don't like numeric indexing either, but I also never understood why the committee allowed numeric indexing of tuples. The way I see it, they're equally wrong, but if we're going to have one, we should have the other for orthogonality's sake.A tuple is an ordered sequence of values. It cannot be discriminated by types, because repetition of a type isn’t special. (Such discrimination has now been added, but it only works for special cases.)A variant has a value of one of several types. As Michael noted, indexing the types fundamentally changes their meaning. Now you don’t have a variant of one of several types, but one of several type-index pairs. The amount of information in the variant value has been measurably increased.
Conceptually, perhaps. But not in any way that actually takes up storage or anything. So it doesn't impact the quality of the variant's implementation.
And if someone can find a use for it, more power to them. Especially if it allows them to put different typedefs in the same variant.
The only thing it does for the implementation is prevent it from re-ordering the elements in the variant.
We should not create an arbitrary restriction on a C++ type, or choose not to implement genuinely useful functionality, based on some external concept of what a `variant` ought to be.
variant<int, int> v{in_place<0>, 42};type_switch (v) ([](int a) { /* handle first int */ },
);
variant<int, int> v{in_place<0>, 42};
type_switch (v) ([](tag<int, 0> a) { /* handle first int */ },
);
Actually, it explicitly allows it; multiple repeated types are distinct states. But it says that, for such a template, using a type-based getter always fails; you have to access the element based on the index. Same goes for emplacement construction.Oh, that’s broken. I don’t want a template that breaks with repeated types. Perhaps I want one template that works with repeated types, and another that diagnoses them, and allows type-based discrimination. But repeated types don’t sound too appetizing at all, especially if they’re not simply deduplicated.
Well, if duplicate types are "deduplicated", then what's the point in allowing them at all? If you're not going to allow duplicate types to be considered separate states, then there's no point in allowing them at all.
A variant in the "null" state has a value, just like a pointer in the "null" state has a value. So even conceptually, `void` is the wrong thing.The variant has a value, but it does not hold a value. A variant in the special error state you mentioned also has a value.But the variant does hold a value. It holds a value that means "nothing meaningful." That value, like "null" for pointers, is a value. It has all of the rights, powers, and functions of a real C++ value. You can create them, copy them, pass them around, and store them.
You can't do any of that with `void`.`void` is not something we should try to use. It's something we should try to avoid using.That doesn’t make sense. It’s not an anti-pattern; it’s a pure and valid concept. It represents something you can’t use. It represents “nothing there.”
I can create a value of type `nullptr_t`. I can create a value of type `nullopt_t`. I can create references to both of those.
I cannot create a value of type `void`. It is not a valid C++ type. It lacks orthogonality with the behavior of C++ types. That lack of orthogonality is what makes using it like this an anti-pattern.
Consider a simple copy visitor class for a variant. All it does is return a copy of the value:
struct CopyVisitor
{
template<T> auto operator()(const T &t) const {return t;}
};
Well, you can't actually do that with `void`, can you? You can't get a reference to `void`, and you can't return a copy of `void`. So you would need to create a special overload solely to handle this case:
struct CopyVisitorAndVoid
{
template<T> auto operator()(const T &t) const {return t;}
void operator()() {return;}
};
What's more, CopyVisitor could have been a lambda; CopyVisitorAndVoid cannot.
Template code has to deal with this kind of "what if T is void" question a lot. The best answer: stop using `void`.
variant<int, string, void> v{42};variant<int, void> w{42};type_switch (v, w) ([](int, int) { ... },
[](int, void) { ... },
[](string, int) { ... },
[](string, void) { ... },
[](void, int) { ... },
[](void, void) { ... }
);
On 2015–09–22, at 9:49 AM, Agustín K-ballo Bergé <kaba...@hotmail.com> wrote:On 9/21/2015 10:25 PM, David Krauss wrote:I prefer an interface that works 100% with definite meaning over one
with contingencies for cases where “you just don’t know.”
I completely agree. The index-based interface is the one that works 100% with definite meaning. The type-based interface is syntax sugar for a limited use case, where you do now.
This is also a good argument as to why magic types should not have special meaning (or rather there should not be magic types). You see `variant<T, U>`, what does it mean? How many members does the variant have? What is the result of default constructor? What are the emplace semantics? What if both `T` and `U` are specializations of this `default_construct<X>` special tag? Etc. For a discriminated union the answer to those and other questions are straight-forward and do not depend on the particulars of `T` nor `U`, giving 100% definite meaning. For a particular implementation of the sum type concept, it "depends”.
On Mon, Sep 21, 2015 at 4:56 AM, Michael Park <mcy...@gmail.com> wrote:Hello, I've been working on an alternative variant proposal. It's still very rough at this point but I've captured the big questions I wanted to answer, and would appreciate feedback from the community.The following are some of the principles the design is based on:(1) union is not a good starting point for a discriminated union. I believe better starting points are:From C++: enum as a special case where each member is a unit type, andclass inheritance where an abstract base class is a discriminated union of its derived classes.From other languages: Sum types from Haskell and ML or enum from Rust.(2) The order of the types specified in variant<Ts...> should not change its behavior.(3) The API should be minimal, useful, and consistent.The following are few design decisions that fall out from the above principles:* The visitation interface is a type_switch expression which looks similar to a regular switch statement,as well as match expressions from functional languages. (1)* Default construction should not construct the first type. (2)* The members should be discriminated by type rather than the index. (2), (3)(i.e. variant<int, string, int> behaves equivalently to variant<int, string>)* There is no special treatment for the null state. (3)This is the work-in-progress in a Google Doc: Variant and it is open for comments.I'm in Seattle attending CppCon this week. Please reach out if you're also here and would like to provide feedback and/or have further design discussions!Thanks,MPark.I find it a little bit hard to follow your proposal - you often list various potential strategies, one of which is your chosen strategy. But it isn't always clear which is the one you chose (or maybe you are choosing more than one). Or possibly I'm just bad at reading and need to re-read it. But giving your chosen strategy a Green title or something similar might help.
I _think_ you are suggesting:- "empty" state exists- "empty" state is used for construction- "empty" state is used for failed assignment(?)- variant<int, string> == variant<string, int> (as much as possible)- variant<int, string, int> == variant<int, string>
If it has an empty state, what's with the null_t stuff? I think that's where I'm most confused.
void F(const variant<int, std::string>& v) {type_switch (v) ([](int) { /* handle int */ },[](const std::string&) { /* handle string */ },);}
void F(const variant<int, std::string, std::null_t>& v) {type_switch (v) ([](int) { /* handle int */ },[](const std::string&) { /* handle string */ },
[](std::null_t) { /* handle null */ },
);}
If I use null_t as one of the types (ie variant<int, string, null_t>), and assignment throws, does the variant become empty, or get the value of a null_t?
If I use int (ie noexcept default constructible) as one of the types ie variant<int, Foo, Bar> and assignment throws, does it become empty, or get the value of int(0)? (ie is null_t more special than int?)
For default variant construction you mention:
"Assuming that default constructability is desirable, it would be desirable for any variant to be default constructible. "The logic is sound, but I'm not sure about the assumption. For some classes (or some coding styles and guidelines), default construction is NOT desirable. If I've decided (in spite of the annoyances) to make Foo not default constructible and Bar not default constructible, I probably don't want variant<Foo, Bar> to be default constructible.
Tony
struct Level { int level; };
struct Percentage { int percentage; };
using Grade = variant<Level, Percentage>;
[snip]
I will personally always be against a null state because the rationale doesn't hold up. I think it's a huge mistake that people believe it is needed. If types involved are noexcept-movable, there is no need for the empty state (this is already acknowledged). If there is a type involved that has a move constructor that can through, we can just require that the user has a noexcept-default-constructible type as one of the specified fields, and we can simply default-construct that internally if a move throws an exception. If neither of these is the case, then the variant can simply be not movable (these requirements are not difficult to meet). This avoids weakening the type's invariants with an unnecessary null state and gives the user more control.
On 2015–09–22, at 8:17 PM, Agustín K-ballo Bergé <kaba...@hotmail.com> wrote:This seems like a forced way to phrase it. The interface works 100% of the time, 20% of the interface has stricter requirements than the rest. This is no different than `std::get<T>` on tuple, or `operator==` on `std::vector`, and I cannot imagine it being surprising. Consider:
I can’t imagine wanting to use indexes, or making the effort to define
the constants needed to do indexing properly. Given a variant<string,
int>, I’m reaching for get<string>, not get<0>. So, for me, the sooner
variant<string, string> throws an error, the better.
It seems to me you are still thinking sum types, not unions.
Furthermore note that when retrieving the value of a future, if the first member of type `std::exception_ptr` is active then a reference to it should be returned, while if the second member of type `std::exception_ptr` is active then it should be rethrown.
On 2015–09–22, at 8:52 PM, Nicol Bolas <jmck...@gmail.com> wrote:I really like the N4542 approach of "empty as error condition". I like it because you can ensure that a `variant` never becomes empty simply by putting copy/move constructors in it that don't ever throw. Therefore, if you put throwing copy/move objects in it, it's up to you to check for errors. And if you don't bother to check for errors, on your head be it.
On 2015–09–22, at 1:06 AM, Nicol Bolas <jmck...@gmail.com> wrote:Conceptually, perhaps. But not in any way that actually takes up storage or anything. So it doesn't impact the quality of the variant's implementation.More states at runtime implies more code to handle the states.And if someone can find a use for it, more power to them. Especially if it allows them to put different typedefs in the same variant.
The only thing it does for the implementation is prevent it from re-ordering the elements in the variant.No, it affects the semantics of having different typedefs in there. Why shouldn’t it be allowed to retrieve a value given a typedef to its type? C++ (and C) don’t work by discriminating typedefs from the aliased type; you’re inventing a need.
Also, users are pressured to use indexing if it’s more robust, which comes down to either magic numbers or naming the states after types.
We should not create an arbitrary restriction on a C++ type, or choose not to implement genuinely useful functionality, based on some external concept of what a `variant` ought to be.We shouldn’t create a type with functionality defined by the arbitrary internal details of a prototype implementation.
I can create a value of type `nullptr_t`. I can create a value of type `nullopt_t`. I can create references to both of those.
I cannot create a value of type `void`. It is not a valid C++ type. It lacks orthogonality with the behavior of C++ types. That lack of orthogonality is what makes using it like this an anti-pattern.That’s the opposite of what orthogonality means. Your argument goes in the direction that disengagement is something that ordinary types should accomplish. Should the user be able to define whether a given type implements engagement or disengagement? Perhaps exception types are all disengaged states?
Template code has to deal with this kind of "what if T is void" question a lot. The best answer: stop using `void`.“Replace void with another type to represent no value — but which has a value” does not follow.std::function and [boost|experimental]::any both use typeid(void) to express their disengaged states.
I don't know what you mean by "tag types". If you're talking about things like iterator categories, those are way too intrusive to use, and can only be used by certain special functions.
Also, what does it matter if it is "impure"? How do you even define "purity" here?I find this:std::variant< std::default_initial< int >, std::string > >to be more expressive than this:std::variant< int, std::string >Defaulting to the first type is just weird and obtuse.
It seems like an implementation detail being foisted on the user.
The current proposal only has one "empty" state, and it can only be caused by copy/move failure. The user can decide if a particular state conceptually represents "being empty", but that's up to the user. The current proposal does not provide some specific type that, when put into the type list, is universally considered being empty.N4542 has a function valid(), but no means to invalidate a variant.
So, it’s up to each user to define their own “disengaged value” type. That’s great for interoperability.Also, std::monostate is described by N4542 as the type of an “empty state.” I’m not reviewing in detail its exact semantics, just now.
Iterating on big proposals full of little details doesn’t seem to be working.
Everyone judges "working" based on whether it leads to the result they want.I’m only judging by the number of iterations and the process used to get from #3 to #4. Also, N4542 still contains enough errors and inconsistencies that #5 seems assured.
We have `any` and `optional` in TS's. Given the construction and reception of N4542, we will likely have `variant`, as well as probably all three in C++17. These classes will fill good and useful niches in the C++ language.That's the only objective definition of "working". So objectively speaking, the process seems to be working.The objective definition of “work” is “people applying the necessary effort to achieve an end.” The process is working because we are. I meant to disparage review sessions resulting in design-by-committee, not anything broader.
TS’es exist to promote rapid, intense review, to speed the process — whatever it really is.
Debate over what remains on the table is common in this sort of situation. It can take a large majority to change the direction of a draft specification, but Eggs.Variant seems to be getting more support than Boost.Variant, and no draft has even been accepted yet. Now we have another prototype as well.
On 2015-09-21 08:50, David Krauss wrote:
> A variant has a value of one of several types. As Michael noted,
> indexing the types fundamentally changes their meaning. Now you
> don’t have a variant of one of several types, but one of several
> type-index pairs. The amount of information in the variant value has
> been measurably increased.
I'm only cursorily following this... my main interest in a standardized
variant type would be something like QVariant, which IIUC is more like
boost::any.
That said, I have to agree with one point being made that it seems
strange to me that the "key" is an index and not a type. Even unions
don't do that; unions "key" on a *name*.
The rule for variant type-list duplicates is arbitrary,
unrelated to other rules in the language,
It’s more brittle. Given a variant<int, T> where T varies with the enclosing library interface, the user can get<T> for any T besides int, and the library can’t stop them from doing so.
I’ll be interested when there’s a discriminated_union template that can store the discriminator in a bitfield.
And what makes you think this behavior is based on "arbitrary internal details of a prototype implementation"? The desire for accessing `variant` by index is not due to implementation details of an implementation. It's a request for greater functionality.
It has nothing to do with the details of any particular `variant` implementation.
And yet, N4542 is not "design-by-committee" at all. It's very much "design-by-Boost-with-minor-additions". Just like `any` and `optional`. And `shared_ptr`. And `function`.
N4542 is not very different from `boost::variant`. I see no evidence of any "design-by-committee" here. Your notion that more iterations = bad just doesn't hold water.
On 22 September 2015 at 09:01, Nicol Bolas <jmck...@gmail.com> wrote:And what makes you think this behavior is based on "arbitrary internal details of a prototype implementation"? The desire for accessing `variant` by index is not due to implementation details of an implementation. It's a request for greater functionality.Yes, it is. The people who wanted that functionality made their case for it in Lenexa, and those of us who participated in the LEWG sessions on variant found their request quite reasonable.It has nothing to do with the details of any particular `variant` implementation.That is correct.And yet, N4542 is not "design-by-committee" at all. It's very much "design-by-Boost-with-minor-additions". Just like `any` and `optional`. And `shared_ptr`. And `function`.
N4542 is not very different from `boost::variant`. I see no evidence of any "design-by-committee" here. Your notion that more iterations = bad just doesn't hold water.The choices are:
- Double buffering in some circumstances
- Empty/invalid state of some sort
- Limit the kinds of types storable in variant
- Terminate when you cannot engage the variant
B) In the case that at least one type has a move constructor that can throw, but one of your types has a default constructor that is noexcept, you can construct that in the even that your variant's move-assignment operator does not propagate an exception. Everything works and is simple and there is no invalid state.
On 22 September 2015 at 09:01, Nicol Bolas <jmck...@gmail.com> wrote:And what makes you think this behavior is based on "arbitrary internal details of a prototype implementation"? The desire for accessing `variant` by index is not due to implementation details of an implementation. It's a request for greater functionality.Yes, it is. The people who wanted that functionality made their case for it in Lenexa, and those of us who participated in the LEWG sessions on variant found their request quite reasonable.It has nothing to do with the details of any particular `variant` implementation.That is correct.And yet, N4542 is not "design-by-committee" at all. It's very much "design-by-Boost-with-minor-additions". Just like `any` and `optional`. And `shared_ptr`. And `function`.
N4542 is not very different from `boost::variant`. I see no evidence of any "design-by-committee" here. Your notion that more iterations = bad just doesn't hold water.The choices are:
- Double buffering in some circumstances
- Empty/invalid state of some sort
- Limit the kinds of types storable in variant
- Terminate when you cannot engage the variant
Boost.Variant does double buffering. Enough committee members are adamantly against double buffering, so the invalid state was considered the best of the alternatives.All the other decisions about variant (default construction, comparison operators, etc.) reached consensus fairly easily.Enough other committee members did not like the LEWG consensus achieved in Lenexa, so the entire discussion will happen again with a larger audience.
I'm happy with the decision to have an "Empty/invalid state of some sort". The only modification I would make would be to call it the empty value, default construct to it,
C) In the case that one of your types has a move constructor that can throw and you do not have a type that has a default constructor that is noexcept, then your overall variant just simply is not move-assignable/copy-assignable. Everything works and is simple and there is no invalid state.
On Tue, Sep 22, 2015 at 11:00 AM, Nevin Liber <ne...@eviloverlord.com> wrote:On 22 September 2015 at 09:01, Nicol Bolas <jmck...@gmail.com> wrote:And what makes you think this behavior is based on "arbitrary internal details of a prototype implementation"? The desire for accessing `variant` by index is not due to implementation details of an implementation. It's a request for greater functionality.Yes, it is. The people who wanted that functionality made their case for it in Lenexa, and those of us who participated in the LEWG sessions on variant found their request quite reasonable.It has nothing to do with the details of any particular `variant` implementation.That is correct.And yet, N4542 is not "design-by-committee" at all. It's very much "design-by-Boost-with-minor-additions". Just like `any` and `optional`. And `shared_ptr`. And `function`.
N4542 is not very different from `boost::variant`. I see no evidence of any "design-by-committee" here. Your notion that more iterations = bad just doesn't hold water.The choices are:
- Double buffering in some circumstances
- Empty/invalid state of some sort
- Limit the kinds of types storable in variant
- Terminate when you cannot engage the variant
I gave what I think is best of all these options (all of the following describe a single option):A) In the case that no types involved have a move constructor that can throw, there is no problem. Everything works and is simple and there is no invalid state.B) In the case that at least one type has a move constructor that can throw, but one of your types has a default constructor that is noexcept, you can construct that in the even that your variant's move-assignment operator does not propagate an exception. Everything works and is simple and there is no invalid state.
C) In the case that one of your types has a move constructor that can throw and you do not have a type that has a default constructor that is noexcept, then your overall variant just simply is not move-assignable/copy-assignable. Everything works and is simple and there is no invalid state.Note that in the above option we never weaken the invariants of our type and we always satisfy the never-empty guarantee. There is never the need for a special invalid state. If users need to guarantee in generic code that their variant is move-assignable, even if one of their types has a move-constructor that can propagate an exception, then all they need to do is put in a field of their choosing, analogous to boost::blank, as I.E. the first field of the variant (which would satisfy "B").This should satisfy everyone's needs and I don't think it's too controversial. It's efficient and gives the user control.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.
On 22 September 2015 at 13:21, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:C) In the case that one of your types has a move constructor that can throw and you do not have a type that has a default constructor that is noexcept, then your overall variant just simply is not move-assignable/copy-assignable. Everything works and is simple and there is no invalid state.That leads to non-portable code. Isvariant<set<int>, unordered_set<int>>
assignable?
On 22 September 2015 at 13:25, Andrew Tomazos <andrew...@gmail.com> wrote:I'm happy with the decision to have an "Empty/invalid state of some sort". The only modification I would make would be to call it the empty value, default construct to it,Which was discussed and rejected.
On Tue, Sep 22, 2015 at 2:21 PM, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:I gave what I think is best of all these options (all of the following describe a single option):A) In the case that no types involved have a move constructor that can throw, there is no problem. Everything works and is simple and there is no invalid state.B) In the case that at least one type has a move constructor that can throw, but one of your types has a default constructor that is noexcept, you can construct that in the even that your variant's move-assignment operator does not propagate an exception. Everything works and is simple and there is no invalid state.That sounded reasonable to me at first, however:I have a drawing program. It can create squares and circles and triangles, and lay them out, group them (hierarchy) etc.The user selects a triangle, and clicks "Change to Circle". The triangle becomes a square.
Is that OK?To me, the only correct answer is the strong exception guarantee, and double-buffering (internal, non-allocated - don't want to bring allocators into this).
If I don't have the strong exception guarantee, I need to implement it externally. ie keep a backup (probably via the undo buffer).
But wait, assignment _didn't_ fail. Or at least I can't tell it failed. It tried to tell me (via the exception) but the exception was swallowed. I don't know that I need to restore to a sane state.
I just read the minutes. It was 10 people in the room and the first vote on that issue was split down the middle.
I think there were a lot of people not present that hold the position that if there is to be a reachable empty state then it should behave like every other type that has such an empty state. I expect the default-construct-to-empty to be reopened at Kona.
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:On Tue, Sep 22, 2015 at 2:21 PM, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:I gave what I think is best of all these options (all of the following describe a single option):A) In the case that no types involved have a move constructor that can throw, there is no problem. Everything works and is simple and there is no invalid state.B) In the case that at least one type has a move constructor that can throw, but one of your types has a default constructor that is noexcept, you can construct that in the even that your variant's move-assignment operator does not propagate an exception. Everything works and is simple and there is no invalid state.That sounded reasonable to me at first, however:I have a drawing program. It can create squares and circles and triangles, and lay them out, group them (hierarchy) etc.The user selects a triangle, and clicks "Change to Circle". The triangle becomes a square.Is that OK?To me, the only correct answer is the strong exception guarantee, and double-buffering (internal, non-allocated - don't want to bring allocators into this).Why? I'm all for having the strong exception guarantee if it makes sense and is implementable without sacrifice, but that's simply not the case here. Realistically we can only efficiently meet the basic exception guarantee in the event that one of the move-constructors can throw. If you need to go back to the previous state when an exception propagates, a user would have to take that into consideration (this is always the case for any type with the basic exception guarantee, and is always tricky when a type has a move constructor that can throw).
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:If I don't have the strong exception guarantee, I need to implement it externally. ie keep a backup (probably via the undo buffer).In practice, a user can store the field via a unique_ptr instead of directly, such that the field in question now has a noexcept move constructor.
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:But wait, assignment _didn't_ fail. Or at least I can't tell it failed. It tried to tell me (via the exception) but the exception was swallowed. I don't know that I need to restore to a sane state.You can tell that it failed. The exception isn't swallowed, it still propagates. The implementation only constructs the fallback type to restore the invariants in the case that an exception does, indeed, propagate. If there is no suitable fallback type, then the user gets a compile error.
On Tue, Sep 22, 2015 at 2:46 PM, Andrew Tomazos <andrew...@gmail.com> wrote:On Tue, Sep 22, 2015 at 8:27 PM, Nevin Liber <ne...@eviloverlord.com> wrote:On 22 September 2015 at 13:25, Andrew Tomazos <andrew...@gmail.com> wrote:I'm happy with the decision to have an "Empty/invalid state of some sort". The only modification I would make would be to call it the empty value, default construct to it,Which was discussed and rejected.I just read the minutes. It was 10 people in the room and the first vote on that issue was split down the middle. I think there were a lot of people not present that hold the position that if there is to be a reachable empty state then it should behave like every other type that has such an empty state. I expect the default-construct-to-empty to be reopened at Kona.Then let's not call it an empty state. Call it an error state (which it currently is).
If it is the construction state, then I need to check for it.
Whereas if it is only on throwing-move, I can ignore it.
On Tue, Sep 22, 2015 at 8:58 PM, Tony V E <tvan...@gmail.com> wrote:On Tue, Sep 22, 2015 at 2:46 PM, Andrew Tomazos <andrew...@gmail.com> wrote:On Tue, Sep 22, 2015 at 8:27 PM, Nevin Liber <ne...@eviloverlord.com> wrote:On 22 September 2015 at 13:25, Andrew Tomazos <andrew...@gmail.com> wrote:I'm happy with the decision to have an "Empty/invalid state of some sort". The only modification I would make would be to call it the empty value, default construct to it,Which was discussed and rejected.I just read the minutes. It was 10 people in the room and the first vote on that issue was split down the middle. I think there were a lot of people not present that hold the position that if there is to be a reachable empty state then it should behave like every other type that has such an empty state. I expect the default-construct-to-empty to be reopened at Kona.Then let's not call it an empty state. Call it an error state (which it currently is).
If it is the construction state, then I need to check for it.Same as std::any, std::function, std::thread, std::all_the_ptrs, etc etc.And what's more with variant you have to branch on the discriminator anyway. It's just one more branch in the switch.For each type T in the variant, the variant can either be holding T or not. Having an empty value doesn't change that.Whereas if it is only on throwing-move, I can ignore it.It's precisely because of the fact that people will ignore it if it is rare that it will cause bugs.Also, I don't think a fully-embraced empty value will necessarily be limited to being reachable via throwing moves in the most elegant design. I think it entails other (good) design decisions.
On Tue, Sep 22, 2015 at 2:54 PM, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:On Tue, Sep 22, 2015 at 2:21 PM, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:I gave what I think is best of all these options (all of the following describe a single option):A) In the case that no types involved have a move constructor that can throw, there is no problem. Everything works and is simple and there is no invalid state.B) In the case that at least one type has a move constructor that can throw, but one of your types has a default constructor that is noexcept, you can construct that in the even that your variant's move-assignment operator does not propagate an exception. Everything works and is simple and there is no invalid state.That sounded reasonable to me at first, however:I have a drawing program. It can create squares and circles and triangles, and lay them out, group them (hierarchy) etc.The user selects a triangle, and clicks "Change to Circle". The triangle becomes a square.Is that OK?To me, the only correct answer is the strong exception guarantee, and double-buffering (internal, non-allocated - don't want to bring allocators into this).Why? I'm all for having the strong exception guarantee if it makes sense and is implementable without sacrifice, but that's simply not the case here. Realistically we can only efficiently meet the basic exception guarantee in the event that one of the move-constructors can throw. If you need to go back to the previous state when an exception propagates, a user would have to take that into consideration (this is always the case for any type with the basic exception guarantee, and is always tricky when a type has a move constructor that can throw).
If I only have basic, I end up writing strong myself, in a more convoluted way. Thus I just rather have strong from the start.
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:If I don't have the strong exception guarantee, I need to implement it externally. ie keep a backup (probably via the undo buffer).In practice, a user can store the field via a unique_ptr instead of directly, such that the field in question now has a noexcept move constructor.Allocation. No thanks.
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:But wait, assignment _didn't_ fail. Or at least I can't tell it failed. It tried to tell me (via the exception) but the exception was swallowed. I don't know that I need to restore to a sane state.You can tell that it failed. The exception isn't swallowed, it still propagates. The implementation only constructs the fallback type to restore the invariants in the case that an exception does, indeed, propagate. If there is no suitable fallback type, then the user gets a compile error.Ah, OK. Not quite as bad then.But in the case of a collection of variants (ie a vector, or my layout document), in order to know _which_ variant failed
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:At that point, a built-in error state (Lenexa variant) seems just as good. Or better, since it can only happen on failure, whereas blank_t can happen throughout the code.
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:As for the ...else compile erorr, like Nevin mentioned, this might be too drastic, making variant unusable.
On Tue, Sep 22, 2015 at 11:44 AM, Tony V E <tvan...@gmail.com> wrote:(I'd hate to find out I need to add blank_t years later when we decide to port. And also update all my visitors. If I should have known that blank_t was needed, well, just build it into variant.)
template<typename ...Args>
using empty_variant = variant<monostate, Args...>;
Why do people want to dictate how I code?
I don't think that's a reasonable rationale. If you start with a basic guarantee, you are not impacting people who only need the basic guarantee, and people who need to restore the old state can generally do so if they need to. On the other hand, if you start with the strong guarantee where it doesn't naturally fit (I.E. requires double storage and internally a way to keep track of which location holds the actual instance), then a user can't avoid that overhead even when they don't need it, and in the case of double storage, you're paying for that overhead everywhere even though you only propagate an exception in an "exceptional" case.
I would have prefered a never-empty variant, provided it could have been as performant as a union+tag and supported all types. Unfortunately it turns out such a thing is impossible, so having an empty-value is the least-worst alternative (in my opinion).
My statement is (and it is a position shared by a lot of people) that *if* variant has a reachable empty value (whether you call it an invalid state, an error state, whatever) *then* it should behave as all the other C++ types do that have such an empty value.
I don't think we should be so quick to jump to an invalid state.
But that's not what we'd be "accepting" if we accepted your idea. We'd be accepting that some people cannot use variant.
I would have prefered a never-empty variant, provided it could have been as performant as a union+tag and supported all types. Unfortunately it turns out such a thing is impossible, so having an empty-value is the least-worst alternative (in my opinion).
On Tue, Sep 22, 2015 at 8:58 PM, Nevin Liber <ne...@eviloverlord.com> wrote:On 22 September 2015 at 13:46, Andrew Tomazos <andrew...@gmail.com> wrote:I just read the minutes. It was 10 people in the room and the first vote on that issue was split down the middle.And there were discussions after that.While we did reach consensus, it was not unanimous (IIRC there was still one dissenter in the room).I think there were a lot of people not present that hold the position that if there is to be a reachable empty state then it should behave like every other type that has such an empty state. I expect the default-construct-to-empty to be reopened at Kona.Oh, I expect every design decision on variant (and possibly optional, since it is a related type, as well as requiring move constructors to never throw, either in the standard library or in the language) to be reopened in Kona.And there is a very real possibility that we won't have variant in the standard at all, if committee members cannot come to consensus on what they are willing to live with.If it drags on I think people will soften their stances.
It was just a little early at Lenexa and the group was too small.
I think the 1000+ messages are a sign that people really want std::variant, so that will motivate consensus in the long run.
On Tuesday, September 22, 2015 at 7:04:01 PM UTC-4, Matt Calabrese wrote:On Tue, Sep 22, 2015 at 3:37 PM, Andrew Tomazos <andrew...@gmail.com> wrote:I would have prefered a never-empty variant, provided it could have been as performant as a union+tag and supported all types. Unfortunately it turns out such a thing is impossible, so having an empty-value is the least-worst alternative (in my opinion).But it *is* possible and is as performant. It only doesn't work for all types if you are talking about my suggestion that certain functions are present iff some operations of the field types are noexcept. Predicating the existence of operations in instantiations of a template on properties of one or more of the arguments that the template is instantiated with is nothing unique to variant, and I don't see it as unreasonable. It's simply what makes sense. Other examples in the standard and in user code just often aren't predicated on a noexcept specification, but rather only on the operation existing. In this case, implementability really does depend on such a specification, so we shouldn't be afraid to respect that requirement. We do not need to introduce an invalid state. When the predicate isn't satisfied, we can simply state that the type isn't move-assignable/copy-assignable. If the user wishes to support that operation, they can get it by putting in their own type (analogous to boost::blank), or by making an optional of that variant, if my other recommendation is considered.I don't think we should be so quick to jump to an invalid state. I see no reason for it and it just complicates things for everyone. Just accept that the predicate which tells you whether or not copy-assignment or move-assignment exists is slightly less trivial than it is for containers.
But that's not what we'd be "accepting" if we accepted your idea. We'd be accepting that some people cannot use variant.
Copy assignment and move assignment is not optional for some people. For entirely reasonable people.
On Tue, Sep 22, 2015 at 4:20 PM, Nicol Bolas <jmck...@gmail.com> wrote:And there is nothing specifically about variant that makes copy/move assignment somehow wrong or unacceptable. Even when the copy/move may fail.
On Tue, Sep 22, 2015 at 4:20 PM, Nicol Bolas <jmck...@gmail.com> wrote:Again, why are you dictating who can and cannot use the type? Why are you disfavoring people with throwing copy/move assignments?
On Tue, Sep 22, 2015 at 4:20 PM, Nicol Bolas <jmck...@gmail.com> wrote:
Unless and until you can get people to agree that (like destructors), move assignment shouldn't be able to throw, your idea favors some programmers over others.
I don't think that's a reasonable rationale. If you start with a basic guarantee, you are not impacting people who only need the basic guarantee, and people who need to restore the old state can generally do so if they need to.
Sure they can use it. Even if they want it copy-assignable/move-assignable they can simply put in an extra type or make it optional. Saying they "can't use it" is like saying users cannot use std::pair with a type that is not copyable because that would imply that the std::pair wouldn't be copyable. That's not true, you just don't get that particular operation if you don't meet the requirements of that operation. The only difference in the variant case is that the predicate which dictates whether or not the copy-assignment or move-assignment operation exists is dependent on an operation's noexcept specification rather than that operation merely being defined. This requirement exists out of necessity, whether people immediately understand that or not.
It's simply a fact and is the underlying issue of all of these discussions -- a move-constructor that can throw prevents us from being able to implement the move-assign or copy-assign operation. Either you simply represent that requirement and leave variant otherwise untainted, or you weaken the invariants of the type.
On 2015–09–23, at 1:06 AM, Nevin Liber <ne...@eviloverlord.com> wrote:On 22 September 2015 at 08:07, David Krauss <pot...@gmail.com> wrote:The rule for variant type-list duplicates is arbitrary,Arbitrary as in either
It’s more brittle. Given a variant<int, T> where T varies with the enclosing library interface, the user can get<T> for any T besides int, and the library can’t stop them from doing so.The only people this is brittle for are the ones who mix both type based indexing with numeric based indexing. That is a tiny fraction of the users of variant.
I’ll be interested when there’s a discriminated_union template that can store the discriminator in a bitfield.The current variant proposal can do that, but why?? That seems way less efficient.
On 22 September 2015 at 15:43, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:I don't think that's a reasonable rationale. If you start with a basic guarantee, you are not impacting people who only need the basic guarantee, and people who need to restore the old state can generally do so if they need to.The only cost to variant for the strong guarantee is space.
One cannot build an efficient strong guarantee on top of a variant with an empty state, because every mutating operation may throw an exception, including swap. This is very problematic.