Thoughts on N4542 std::variant

Visto 1.161 veces
Saltar al primer mensaje no leído

Markus Grech

no leída,
5 jun 2015, 4:17:515/6/15
a std-pr...@isocpp.org
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.

Markus

Miro Knejp

no leída,
5 jun 2015, 7:22:025/6/15
a std-pr...@isocpp.org
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.
Agreed completely. This seems like an arbitrary decision just for the sake of having a DefaultConstructible variant. I am sure it would not be surprising to anybody at all if someone were to change the signature of a variant by adding a new type at the front and suddenly having all default constructed variants completely change the behavior of the program with no warnings or errors.
  • 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.
Also considering “Monostate” is a software design pattern that is completely unrelated

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.

Although I do think the empty state is a terrible decision that needlessly complicates the interface. We already have a proposed type that can be empty and it’s called optional. That role is served. Just because some people think it’s a good idea to have throwing move constructors means everybody using std::variant now has to keep this tiny detail in their head all the time. Getting rid of this just plays well with all the advice out there to make move operations nothrow.

I’d rather have a variant that cannot store every type under the sun, which is something the compiler can enforce and inform me about before it’s too late, than having to constantly worry about it being possibly empty.

This is insane!

Nicol Bolas

no leída,
5 jun 2015, 11:52:045/6/15
a std-pr...@isocpp.org
On Friday, June 5, 2015 at 4:17:51 AM UTC-4, Markus Grech 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.

Well... you could just pretend it doesn't have a default constructor ;)

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.

Without default construction, it would be exceedingly difficult to create std::arrays of variants, as well as other types that don't make it easy to avoid default construction. You might say that those things are problems that should be solved in and of themselves. But we have to live within the limitations of the language that exists, not the language we would like to use.

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.


Monostate and default construction are separate issues. The only design consideration they share is the idea of default construction making the variant empty. Which is a bad idea.

The monostate design (as I understand it) is purely as an error-handling mechanism. Variants are intended to only assume this state if a copy/emplace/etc operation could not be performed. Thus, if you want "empty" to be a legitimate state for a variant, that's something you have to provide as one of the possible variant types.

Think of "monostate" as "invalid"; that's how the proposal describes its behavior. There are numerous operations where the precondition is `variant parameter is valid`, so you can't even call them unless the variant is valid. I think this is a good thing. It means that you don't have to consider the possibility of an empty variant unless it contains a type that can throw on copy/move/emplace. That is, its fairly easy to avoid.

`monostate` and invalid are, and should be, rare conditions.

Matthew Woehlke

no leída,
5 jun 2015, 11:54:105/6/15
a std-pr...@isocpp.org
On 2015-06-05 04:17, Markus Grech wrote:
> 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.

While QVariant is probably closer to boost::any, it may still be worth
considering the precedent: QVariant has a null state.

(I agree, having default construction use the first type seems...
arbitrary.)

--
Matthew

Alisdair Meredith

no leída,
5 jun 2015, 12:02:465/6/15
a std-pr...@isocpp.org
Default construction initializing the first type is modeled on the way
that a union works in the language, where the first element of the
union is value-initialized if a union needs value initialization.

If a variant is thought of as ‘a union that knows its active element’,
then I think this makes sense. If the variant is thought of as something
quite distinct from C++ unions, then I agree it might seem arbitrary.

AlisdairM
> --
>
> ---
> 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/.

Izzy Coding

no leída,
6 jun 2015, 4:39:596/6/15
a std-pr...@isocpp.org
Surely a std::variant should only be default constructable if all its allowed types are default constructable?

Also I would suggest that default construction should initialise to being empty which could be checked using a member function empty() like most other STL container?

Just my opinion

Thiago Macieira

no leída,
6 jun 2015, 7:42:376/6/15
a std-pr...@isocpp.org
If std::variant, as described, should operate like the unrestricted union of
the types it contains, then it should have a default constructor under the
same rules. If the union has the default constructor deleted, so should
std::variant.
--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Farid Mehrabi

no leída,
6 jun 2015, 11:52:376/6/15
a std-proposals
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.

regards,
FM.
--
how am I supposed to end the twisted road of  your hair in such a dark night??
unless the candle of your face does shed some light upon my way!!!

Nicol Bolas

no leída,
6 jun 2015, 13:12:336/6/15
a std-pr...@isocpp.org


On Saturday, June 6, 2015 at 11:52:37 AM UTC-4, Farid Mehrabi wrote:
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?'

boost::variant doesn't have a blank state. You can give it a blank state, but only by putting `boost::blank` in the argument list. But that's a user choice, not something every variant has.

Consider two variants. Variant A forces a null state, while Variant B (like boost::variant) allows you to have one if you want, but makes it an argument in the list.

Let's say I'm in a situation where all my variants are filled. I don't default construct anything, and none of my move constructors throw. So why should my visitation code have to consider the possibility of being empty? Why should my visitor functors have to consider `std::blank_t` or whatever as one of the choices, when the variant will never have that value? Variant A is decidedly less useful to me than Variant B.

Now, let's consider the situation where I want a variant to potentially be empty. Well, Variant A gives it to me by default. For Variant B... I just put it `std::blank_t` in the template argument list, and it's there.

`std::variant` is nice in that it doesn't really have an "empty" state; it has an "invalid" state. It only gets this state when copy/move fails in some not-easily-recovered way.
 
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...>`.

So what's the need for such arbitrary compatibility?

Some people like to think of `optional` as a nullable variant with only one non-null state. Other people think of it as a container that could have zero or one element. Neither view encapsulates how users will want to use the object. This kind of high-concept thinking can lead to very bad interfaces.

Miro Knejp

no leída,
6 jun 2015, 16:23:076/6/15
a std-pr...@isocpp.org


Am 06.06.2015 um 13:42 schrieb Thiago Macieira:
> On Saturday 06 June 2015 01:39:59 Izzy Coding wrote:
>> Surely a std::variant should only be default constructable if all its
>> allowed types are default constructable?
>>
>> Also I would suggest that default construction should initialise to being
>> empty which could be checked using a member function empty() like most
>> other STL container?
> If std::variant, as described, should operate like the unrestricted union of
> the types it contains, then it should have a default constructor under the
> same rules. If the union has the default constructor deleted, so should
> std::variant.
I agree that if the equivalent union has a deleted default constructor
so should the variant, however the "initialize the first member" rule
only goes when value- and zero-initializing a union and there is a
subtle difference to default-initialization. I find it to be one of
these surprising things where people look at their code for hours and go
"wtf is going on". How many people are familiar with the initialization
rules for unions?

I can already see the confusions coming where someone changes variant<T,
U> to variant<U, T> and things go wrong. I am not speaking about "wrong"
as in "get<0>(v) now returns a different type" which can be cought at
compile time. I am speaking about the kind of "wrong" where suddenly all
default-initialized objects have a different state. This is along the
same category of stealthy behavior changes implict conversions cause and
are mostly avoided for. I think this does more harm than good in the
long run.

Sean Middleditch

no leída,
7 jun 2015, 2:57:457/6/15
a std-pr...@isocpp.org
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.

That said, if variant is massively more complicated than optional, then perhaps the argument should be that users shouldn't need to include far more machinery than they need for compile-time reasons. In which case there'd be an actually good argument to reimplement optional as its own thing, assuming we stick our heads in the sand and pretend that C++'s core language is incapable of ever changing ever again.

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.

Nicol Bolas

no leída,
7 jun 2015, 3:31:347/6/15
a std-pr...@isocpp.org
On Sunday, June 7, 2015 at 2:57:45 AM UTC-4, Sean Middleditch wrote:
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?

Interface and implementation are not the same thing. A repetition of one does not require a repetition of the other.
 
Why make users include two big headers with a bunch similar templates if they happen to use both library facilities?

Because they don't have similar templates, similar library facilities, or similar use cases.
 
Learning not to needlessly redesign the wheel is one of the first instincts we try to instill in first-year engineering students.

And learning the difference between a wheel and a donut is also something I would hope would be instilled in "first-year engineering students". Just because something is round and has a hole in the middle doesn't make it a wheel.

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

No, we have not "discussed" this; you have asserted it as though it were fact. You declare them to be a functional programming concept; the rest of us say that they're C++ types, all of whom have very different uses.

Something that would qualify as "discussion" of this point would be for you to provide evidence to back up your assertion. Not Haskell or some other non-C++ language. Not unquestionable functional programming wisdom, brought down from Mt. Sinai, and carved into stone by the very finger of God. An actual C++ interface, where it is semantically reasonable for variant, expected, and/or optional to all interoperate as a function parameter or return type.

Show me an actual problem that C++ programmers would face that would be solved by variant, expected, and/or optional sharing identical, interoperable interfaces and implementations.

Here's a bit of argument from the "they're all different" side.

I present the Zero-One-Infinity rule. Every type in C++ conceptually exists in a single state: its current type. optional, expected, and variant all semantically change this, adding some number of alternate states. And they each correspond to one of the three items in the aforementioned rule.

`optional` represents Zero: the "state" that is a lack of state. The object exists or it does not.

`expected` represents One: a single alternative state. The object exists or a single alternate object exists.

`variant` represents Infinity: the object can have infinitely many alternate states. The object exists or one of potentially infinitely many alternate objects exist.

Each concept is distinct and therefore deserves its own type, with whatever interface is appropriate to their concept.

You are dangerously close to the logic behind `vector<bool>`: a space-optimized dynamically allocated bit array masquerading as a `vector`, simply because the interface happened to have certain superficial similarities to `vector`.


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.

I never claimed that it was easy; only that you were free to do so ;)
 

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.

And we're discussing this in a thread that's talking about a pure library proposal, not a language change.  Fixing the general problem of initialization vs. construction universally is out-of-bounds for this discussion. Thus, the proposal must work within the world as it exists today.

Vicente J. Botet Escriba

no leída,
7 jun 2015, 13:40:317/6/15
a std-pr...@isocpp.org
Le 06/06/15 19:12, Nicol Bolas a écrit :
variant, optional, expected and future are all sum types as vector, list,  .... are containers. I don't mind whether one can be implemented in function of the other, but some coherency must be preserved on the interfaces.

When each one of this concrete types provide a common sum type interface (or we map each concrete type as a sum type interface) we are able to build new algorithm, abstraction on top of this common interface independently of the concrete type.

This doesn't mean that the final user can use interchangeably variant, optional, expected. But there are a lot of thing to do with the sum type common interface.

One of the common operations on sum types is to apply (visit) an hetorgeneous function to all the elements. Currently the variant proposal includes a visit function, expected has fmap/bind/catch_error and optional has no visitation function.
We need a match function that works on all these types. This function can be a non-member function or a member one.
This match function could be naturally extended to variadic sum types (that must of course be a non-member one).

The other minimal operation on sum types is to construct one instance of a sum type from one instance of one of its possible elements.

Once you a common and minimal interface for sum types, you can build anything on top of this minimal interface independently of whether we have variant, optional, expected or whatever other sum type we can have on the future.

optional<T>, expected<T>, future<T> and why not variant<T,E> or variant<E,T> where E is fixed and means an Error are in addition monads which open other extension (but this out of the scope of this thread).

These is the kind of design some of us are suggesting/proposing. Seen concrete types as instances of a Concept helps to identify its interface.

Now responding the PO, we can have two kind of variants, one that can be empty and one that can not be empty.
The first can be seen as optional<variant<Ts>> and sure we can have a better implementation for this concrete type. The question is if we need a specific optional_variant name or if we want to specialize the optional when the type is variant. Another way to see it is as a variadic optional

    optional<T1, ..., Tn>

Vicente

Vicente J. Botet Escriba

no leída,
7 jun 2015, 13:58:567/6/15
a std-pr...@isocpp.org
Le 05/06/15 10:17, Markus Grech a écrit :
We have two orthogonal interface decissions that could interact
    * 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.

You are advocating for option B. I believe that option B would be represented better by

    optional<variant<Ts...>>

with a possible specialization  implementation

or by a variadic optional version

    optional<Ts...>

All these concrete classes share a lot in common and I would expect that the library implementor would make use of an implementation class that covers the common parts.

Vicente

Vicente J. Botet Escriba

no leída,
7 jun 2015, 14:04:557/6/15
a std-pr...@isocpp.org
Le 07/06/15 19:58, Vicente J. Botet Escriba a écrit :
Le 05/06/15 10:17, Markus Grech a écrit :
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.


We have two orthogonal interface decissions that could interact
    * 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.
BTW, option A could be simulated with the current proposal by using as first alternative a class that is no constructible at all.

struct non_constructible {
    non_constructible() = delete;
};

template <class ... Ts>
using non_default_constructible_variant<non_constructible, Ts...>


Vicente

Vicente J. Botet Escriba

no leída,
7 jun 2015, 14:07:207/6/15
a std-pr...@isocpp.org
Le 06/06/15 10:39, Izzy Coding a écrit :
> Surely a std::variant should only be default constructable if all its allowed types are default constructable?
An to what it should default construct?
>
> Also I would suggest that default construction should initialise to being empty which could be checked using a member function empty() like most other STL container?
This default construction corresponds to a different type. And in this
case we don't need that all the types are default constructible.

Vicente

Vicente J. Botet Escriba

no leída,
7 jun 2015, 14:55:597/6/15
a std-pr...@isocpp.org
Le 06/06/15 22:23, Miro Knejp a écrit :
In the same way we would have tagged_tuple (See [1] last draft of the
Range TS) we could have tagged_variant. As far as we access
tagged_variant by their tags tagged_variant<T, U> and tagged_variant<U,
T> would behave almost the same.

If the user don't want the varaint to be default_constructible it could
use non_default_constructible_tagged_variant<T, U> or
non_default_constructible_tagged_variant<U, T>.

Having the good building blocks would allow us to provide whatever the
user wants.

A different question is what the C++ standard library would use on a
particular case. But this question should be answered in a case by case
bases.

Vicente

[1] https://github.com/ericniebler/stl2/blob/master/DXXXX.pdf)

Farid Mehrabi

no leída,
15 jun 2015, 16:13:4015/6/15
a std-proposals
​A lot of effort has been put together to make option C possible and I do appreciate that, but why should we set option B aside while it is much easier to implement and more generic?
recalling that a variant assignment generally consists of 3 steps:

1. back up destination.
2. copy source.
3. restore destination if 2 fails.

consider Option C (direct porting of boost::variant into std):
Q: what happens if during an assignment operation, copy from source throws and the restoration move in the destination throws too?​
 A: either a an uncaught exception or undefined behavior in a blank-less variant.
Option B is not only easier,but also less dangerous: if copy fails and restoration fails too, reset to blank(null,empy ...).

A worse scenario is when the backup move throws during the assignment;  with option C it means the sky collapsing, whilst with option B the variant can reset to blank, a subsequent test of blankness can be used to check whether or not the assignment has succeeded.

By the way what are the benefits of the never empty promise of option C? What might go wrong if the variant is in blank state? visitors might eventually ignore the null state or be forced to do something.

And you have actually pointed out one of my concerns about overhead: the 'optional<variant<...>>'. With option B optional variant is just a thin API wrapper with no overhead on the variant, but with option C it is just paying twice the essential price.

regards,
FM.

Matthew Woehlke

no leída,
16 jun 2015, 11:26:4916/6/15
a std-pr...@isocpp.org
On 2015-06-15 16:12, Farid Mehrabi wrote:
> And you have actually pointed out one of my concerns about overhead: the
> 'optional<variant<...>>'. With option B optional variant is just a thin API
> wrapper with no overhead on the variant, but with option C it is just
> paying twice the essential price.

No; there may be a semantic difference between not having a value and
having a variant value that is empty. (OTOH you'd probably rarely if
ever see such a construct. Though it could be similarly argued that if
nullability matters, you should always have blank_t as the first type.)

I would expect 'optional<optional<T>>' to not collapse into
'optional<T>' for that reason. Neither should 'optional<variant<...>>'
collapse into 'variant<...>'.

--
Matthew

Tony V E

no leída,
16 jun 2015, 13:25:4916/6/15
a Standard Proposals
Move operations shouldn't throw.  So we shouldn't add an empty state to ALL variants just for the rare case of throwing move.
And more rare than the types with throwing moves, is those types actually throwing on move.  ie even if a type has a throwing move, it will probably never throw (typically it means you can't even allocated a few bytes - at this point you are probably screwed anyhow).

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.

That leaves empty only being worthwhile for default-construction.  I think you can argue that, but the never-empty has benefits as well. So it is left for the user to decide (by using a specific 'blank' type).

Tony

Farid Mehrabi

no leída,
19 jun 2015, 12:27:3619/6/15
a std-proposals
​never means never not almost never. and we can`t be sure on what circumstances the move ctor may throw, and it is not just about the ctor; backup/restoration involve destruction of the source/temporary, either of which may throw.
 

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.

​if one is sure about the nothrow promise, then [s]he does not need to check for blankness; don`t pay for what u don`t use.
But as library programmers we must care even 4 very rare cases in our design.


So I don't think the empty state is worth it for throwing moves.

​actually the assignment can be simplified to:

1. destruct original.
2. copy source
3. set to blank in case of any sort of failure.

​much easier than the never-empty promise.

On second thought ​I even considered option A and it looked even simpler to implement and more basic, now I can see why boost designers called for proposals on policy-based variant design.

regards,
FM.

 

Patrice Roy

no leída,
19 jun 2015, 12:32:2219/6/15
a std-pr...@isocpp.org
I hope for users that the «assignment simplified to» as you present it takes care of self-assignment (a = a; for some a), otherwise, with the algorithm presented here, they're in for a surprise :)

--

Farid Mehrabi

no leída,
19 jun 2015, 13:06:0419/6/15
a std-proposals
is that so hard to guard against self assignment? even a novice like me can do it. I am leaving the details for the sake of  main point. but thanx for reminder.

regards,
FM.

Michael Park

no leída,
21 sept 2015, 4:56:1121/9/15
a ISO C++ Standard - Future Proposals
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, and
       class 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.

David Krauss

no leída,
21 sept 2015, 5:55:3221/9/15
a std-pr...@isocpp.org
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, and
       class 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.

Michael Park

no leída,
21 sept 2015, 6:59:2421/9/15
a ISO C++ Standard - Future Proposals


On Monday, September 21, 2015 at 2:55:32 AM UTC-7, David Krauss wrote:

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.


I think using union as the underlying storage for variant is necessary at the implementation level to support constexpr (Agustín Bergé demonstrates this with eggs-variant).
But semantically, I don't think it should inherit union's behaviors such as the default constructor attempting to default construct the first type.
       From C++: enum as a special case where each member is a unit type, and
       class 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?

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.
  (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.
 
Yes, I agree that it would be ideal to sort the types. Meanwhile, I think variant should still be order insensitive.
For example, variant<int, std::string> and variant<std::string, int> should default-construct to the same state, if default-constructible at all.

Additionally, I think the index-based operations will be even less useful once the types are ordered at compile-time.
  (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 ...>.
 
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"?

Thanks for your quick feedback!

MPark.

Nicol Bolas

no leída,
21 sept 2015, 8:15:2321/9/15
a ISO C++ Standard - Future Proposals


On Monday, September 21, 2015 at 5:55:32 AM UTC-4, David Krauss wrote:

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.

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.
 
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 ...>.

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.

Nicol Bolas

no leída,
21 sept 2015, 8:17:0621/9/15
a ISO C++ Standard - Future Proposals
On Monday, September 21, 2015 at 8:15:23 AM UTC-4, Nicol Bolas wrote:
Personally, I don't see why it's so important to have a few default behaviors based on the ordering in the list.

Sorry; I said that wrong. That should be:

Personally, I don't see what's wrong with having a few default behaviors based on the ordering in the list.

David Krauss

no leída,
21 sept 2015, 8:18:5921/9/15
a std-pr...@isocpp.org
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.
Yes, this is bad. It’s not quite so bad for C-style unions, which get a deleted default constructor if any member is not trivially constructible. The construct-first-member behavior only comes back when using aggregate initialization.
  • 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.

  • enable the default constructor only if variant is nullable
This seems a bit draconian to me.

  • set the variant to the null state rather than the valid, unspecified state on assignment failure if variant is nullable
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.

  • provide a operator bool() if variant is nullable
Yes; operator bool should simply always return true if not 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

Oh, nice!

I don’t see any mention of reference types, which are supported in N4542.

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”?

Well, I presumed that no implementation would be forthcoming so close to the deadline, so I meant to encourage you to cut the fat and focus on new material. But, I agree that a comprehensive prototype deserves a comprehensive proposal. Unfortunately I’m a bit busy this week to review your library in detail, but best luck to you!

David Krauss

no leída,
21 sept 2015, 8:24:2621/9/15
a std-pr...@isocpp.org

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.

Ah, oops, all those examples either aren’t erasures or are nullable.

Anyway, undefined behavior sounds good enough to me. Throwing move constructors are evil, and not something that well-behaved programmers should pay for.

Nicol Bolas

no leída,
21 sept 2015, 8:40:1221/9/15
a ISO C++ Standard - Future Proposals,pot...@mac.com


On Monday, September 21, 2015 at 8:18:59 AM UTC-4, David Krauss wrote:

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.)

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.
 
I’m not going back just now to review N4542, but I thought I saw it forbid including the same type twice.

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.
 
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?

Because `void` is not a normal C++ type.
 
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.

Exactly. And the "null" state of a variant should be able to do all of those things.

The variant's "null" state should only be considered "null" by decree of the user. The variant itself should only see it as just another state, just as visitors should only see it as just another state. It's "null" because the user decides that this value means that it has no value.
 
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.

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.

David Krauss

no leída,
21 sept 2015, 8:40:5121/9/15
a std-pr...@isocpp.org
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.

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.

You know that’s easier said than done, especially when the compiler won’t flag it for you.

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.

David Krauss

no leída,
21 sept 2015, 8:51:0421/9/15
a std-pr...@isocpp.org
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.”

Anthony Williams

no leída,
21 sept 2015, 9:58:2221/9/15
a std-pr...@isocpp.org
Hi,

In case anyone missed it, I wrote up a lot of my thoughts about variant
in a blog post a couple of months ago:

https://www.justsoftwaresolutions.co.uk/cplusplus/standardizing-variant.html

I posted an implementation on bitbucket:

https://bitbucket.org/anthonyw/variant

I strongly dislike the undefined-behaviour on empty property of the
N4542 variant. If we're going to allow an empty state it should have
defined behaviour.

Not allowing an empty state requires that you can always construct an
object of a type from the list, or you double-buffer. Both of these have
costs.

constexpr compatibility comes at a cost. My implementation is
constexpr-compatible, but it can end up bigger than a
non-constexpr-compatible implementation due to alignment issues.

Anthony
--
Author of C++ Concurrency in Action http://www.stdthread.co.uk/book/
just::thread C++11 thread library http://www.stdthread.co.uk
Just Software Solutions Ltd http://www.justsoftwaresolutions.co.uk
15 Carrallack Mews, St Just, Cornwall, TR19 7UL, UK. Company No. 5478976

Patrice Roy

no leída,
21 sept 2015, 11:58:1621/9/15
a std-pr...@isocpp.org
Just a note, as it's an old issue and might not be a design goal for variant, but if we seek to replace discriminated unions with this, not supporting more than one occurrence of a given type might be a problem due to type aliases in real code. If we forbid types that occur more than once in a standard variant, then some union-based code in applications such as low-level sockets stuff will not be part of the std::variant use-case.

Of course, maybe it's what we want, but it's something to think about.

Cheers!

Anthony Williams

no leída,
21 sept 2015, 12:03:5521/9/15
a std-pr...@isocpp.org
On 21/09/15 16:58, Patrice Roy wrote:
> Just a note, as it's an old issue and might not be a design goal for
> variant, but if we seek to replace discriminated unions with this, not
> supporting more than one occurrence of a given type might be a problem
> due to type aliases in real code. If we forbid types that occur more
> than once in a standard variant, then some union-based code in
> applications such as low-level sockets stuff will not be part of the
> std::variant use-case.
>
> Of course, maybe it's what we want, but it's something to think about.

Agreed. The basic example that's been passed around is
variant<size_t,unsigned long>, but people also raised the issue with
variant usage for low-level interfaces.

Matt Calabrese

no leída,
21 sept 2015, 12:58:0321/9/15
a ISO C++ Standard - Future Proposals
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, and
       class 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 Mon, Sep 21, 2015 at 1:56 AM, Michael Park <mcy...@gmail.com> wrote: 
  * There is no special treatment for the null state. (3)

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.

Nicol Bolas

no leída,
21 sept 2015, 13:06:0021/9/15
a ISO C++ Standard - Future Proposals
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.
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`.

Nicol Bolas

no leída,
21 sept 2015, 13:30:5221/9/15
a ISO C++ Standard - Future Proposals,pot...@mac.com
On Monday, September 21, 2015 at 8:40:51 AM UTC-4, David Krauss wrote:

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.

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. Even if you sort the parameter pack and derive from a base class instantiated on the sorted parameter list, the two sister types in the hierarchy are still not the same type.

You could try to make them seem like the same type by allowing copy and move construction/assignment from one type to a matching type, as well as provide implicit conversions from one to the other. But `std::is_same` will never return true for them.

I just don't see the point of making variant implementations do all that work, when you don't really get equal types out of it. You only get an illusion of equality.
 
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 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 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?

Orthogonality. I covered why this is important in a previous post.
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?

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.
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.)

No, it does not. But it does explain why they started with the design they did, and why they don't strictly need an established implementation of exactly N4542 in order to proceed.

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.
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.

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.

You want `variant`, `any`, and `optional` to all share the same interface. You want them all to be considered specializations of functional programming "sum types" and therefore conceptually interchangeable to some degree. Therefore, a process that doesn't lead to that is not "working" to you.

I want `variant` to not be required to have an empty state (and if it absolutely must, it must be orthogonal with all other types. IE: not `void`). And thus, any process that leads to a `variant` without those properties is not "working" to me.

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.

Tony V E

no leída,
21 sept 2015, 14:07:1821/9/15
a Standard Proposals
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, and
       class 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.
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



David Krauss

no leída,
21 sept 2015, 20:19:2921/9/15
a std-pr...@isocpp.org
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.

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.

Indeed. If allowed, they make the template more tolerant, so you don’t need to worry that it will reject the type-list or fail to retrieve a value of a duplicate type.

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`.

There is a wrapper object and there is an object inside the wrapper object. Two different things.

A disengaged wrapper holds no object at all. Whether that wrapper value is copyable or not depends on the wrapper. But you cannot get a reference to the internal object; this is what distinguishes engagement from disengagement.

Copying a disengaged wrapper does not imply copying a void value.

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?

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:

Attempting to get something that doesn’t exist is signaled by throwing an exception. This looks like a job for bad_variant_access.

What can you do with null_t that’s generically uniform with things that represent actual values? Once you’ve returned null_t by value, now what? Will every user remember to support null_t in their visitor templates when it’s never mentioned in the type-list?

Use if(v) to avoid visiting disengaged wrappers.

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.


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?

The sort order should be implementation-defined; the fact of sorting should not be.

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.

Not for alias templates. They implement many-to-one mapping just fine.

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.

If default_initial< int > is defined to be an incomplete type (undefined class), then there’s no inconsistency that variant wouldn’t be able to hold it.

Orthogonality. I covered why this is important in a previous post. 

You’re arguing for non-orthogonality, or uniformity, that a disengaged wrapper is really still holding something.

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.

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.

Eggs.Variant has a disengaged default state expressed by typeid(void), and no distinction of duplicates. It’s even cited by N4542.

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.

variant and any have more than minimal overlap, however you want to classify them.

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.

Agustín K-ballo Bergé

no leída,
21 sept 2015, 20:42:4921/9/15
a std-pr...@isocpp.org
On 9/21/2015 9:19 PM, David Krauss wrote:
>
>> On 2015–09–22, at 1:06 AM, Nicol Bolas <jmck...@gmail.com
>> <mailto:jmck...@gmail.com>> wrote:
>>
>> 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.
>
> Eggs.Variant has a disengaged default state expressed by typeid(void),
> and no distinction of duplicates. It’s even cited by N4542.

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:

As a discriminated union to replace low-level uses of `union`,
Eggs.Variant has to treat `eggs::variant<int, int>` as having two
distinct members of type `int`.

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).

Either approach can be build on top of the other with a lot of syntactic
noise but no real performance overhead. I went with supporting
duplicated types as distinct members because that's what a `union` does:

union { int x; int y; };

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.

Regards,
--
Agustín K-ballo Bergé.-
http://talesofcpp.fusionfenix.com

David Krauss

no leída,
21 sept 2015, 21:25:4221/9/15
a std-pr...@isocpp.org
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:

Sorry, I saw “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.

It sounds like discriminated unions (going back to C) and sum types (in functional languages) are different concepts. But, note that discriminated unions don’t necessarily have a 1:1 mapping between discriminator values and member names. For a type that represents a FSM (with some variables specific to each “modal” state), I might not even want consecutive numbering. It makes more sense to have a list of index-type pairs, forbidding repeated indexes.

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).

I prefer an interface that works 100% with definite meaning over one with contingencies for cases where “you just don’t know.”

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.

Strong typedefs? Yes, that would be a good solution.

Larry Evans

no leída,
21 sept 2015, 21:44:2421/9/15
a std-pr...@isocpp.org
On 09/21/2015 03:56 AM, Michael Park 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.
[snip]
> * The members should be discriminated by type rather than the index.
[snip]
The following post:

http://article.gmane.org/gmane.comp.lib.boost.devel/168974

suggests there's a need for descrimination by index.
Also, it exists type type theory:

https://en.wikipedia.org/wiki/Tagged_union

which suggests to me that there is some utility in it.

-regards,
Larry



Agustín K-ballo Bergé

no leída,
21 sept 2015, 21:49:4921/9/15
a std-pr...@isocpp.org
On 9/21/2015 10:25 PM, David Krauss wrote:
>
>> On 2015–09–22, at 8:42 AM, Agustín K-ballo Bergé
>> <kaba...@hotmail.com <mailto:kaba...@hotmail.com>> wrote:
>>
>> 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.
>
> It sounds like discriminated unions (going back to C) and sum types (in
> functional languages) are different concepts.

Definitively.

>> 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).
>
> 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".

Michael Park

no leída,
21 sept 2015, 23:10:5821/9/15
a ISO C++ Standard - Future Proposals


On Monday, September 21, 2015 at 9:58:03 AM UTC-7, Matt Calabrese wrote:
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.

Thanks for the feedback, I'll try to simplify the discussion a little bit.
 
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, and
       class 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.

As I mentioned above, I agree that union is the required storage mechanism as far as implementation is concerned. Semantically however, I don't believe that it needs to model a union.
  
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.
 
I was basing this on the principle that order shouldn't matter. I think the rule can be consistent and simple by default constructing into the valid, unspecified state.
I argue that this is no different than int i; being in an unusable state. The expectation is that such int be assigned to something before it is used,
or the general guideline of course is to explicitly initialize it to something. I believe the same applies to a default-constructed variant.
Assign it to something before it is used, but always prefer to explicitly initialize it.

Michael Park

no leída,
21 sept 2015, 23:46:1421/9/15
a ISO C++ Standard - Future Proposals


On Monday, September 21, 2015 at 10:06:00 AM UTC-7, Nicol Bolas wrote:
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.

I think once you get to a variant with duplicate types, you get to a point where you don't get any benefit from using a variant over a union.

Given variant<int, int>, the only way to initialize it is with index-based constructor: variant<int, int> v{in_place<0>, 42};
and to retrieve the value, index-based get would be usedget<0>(v).

I argue that this is no better than union { int x; int y; } u; initializing with u.x = 42; and retrieving by u.x.
I suppose you do get the runtime type-check from get, but in terms of usability, the user still needs to keep track of which member is active.

Furthermore, while the name => index transition seems natural, it's not actually the same thing.
In union { X x; Y y; } u; the expression u.x refers to the same x even if we were to re-order the members as union { Y y; X x; } u;
This is not the case with variant<X, Y> v; as the expression get<0>(v) refers to a different value if the order changes: variant<Y, X>.
Note on the other hand, that get<X>(v) retrieves the same value regardless of the order as desired.

For variant to be better than union, visitation on such variant must be allowed. But how would it be supported?
Should the order of the handlers matter?

variant<int, int> v{in_place<0>, 42};
type_switch (v) (
  [](int a) { /* handle first int */ },
       [](int b) { /* handle second int */ } 
);

Or should they be required to be tagged with their index?

variant<int, int> v{in_place<0>, 42};
type_switch (v) (
  [](tag<int, 0> a) { /* handle first int */ },
       [](tag<int, 1> b) { /* handle second int */ } 
);

I think this becomes unnecessarily complicated for a problem that would be better solved by introducing new types that
wrap these ints which can provide meaningful names anyway. 
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.

The point of allowing them is to facilitate generic programming. Otherwise it would be required that the user would need to deduplicate anytime they have a variant<Ts...>.
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`.

I agree that void cannot be used. In the single dispatch case, it's not even that bad. However, it becomes a real problem when considering multi-dispatch.
The following does not work:

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) { ... }
);

David Krauss

no leída,
22 sept 2015, 0:51:5822/9/15
a std-pr...@isocpp.org
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.

That’s saying that 80% of it works 100% of the time, or that 100% works 80% of the time. The type-based accessor is part of the same class interface.

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.

Or, let it Just Work™. If selecting the first element is good enough for default construction (though I’m not saying it’s so), then selecting the first T is good enough for a type-based getter. Simply presume that the user is using type-based interface consistently and never indexing.

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”.

Magic is bad. But a variety of user preferences have to be signaled somehow.

I think the best way through is to create an essential subset of sum-type functionality, and then enable the other features by magic and/or separate classes.

Vicente J. Botet Escriba

no leída,
22 sept 2015, 3:15:5022/9/15
a std-pr...@isocpp.org
Le 22/09/15 02:19, David Krauss a écrit :
>> 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.
Right, but

if(v) ...
else ....

is not better.
>
>> 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.
I agree that supporting duplicated types will force the use of indexes.
>
>> 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.
+1
>
>> 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.
> Indeed. If allowed, they make the template more tolerant, so you don’t need to worry that it will reject the type-list or fail to retrieve a value of a duplicate type.
>
>> 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`.
> There is a wrapper object and there is an object inside the wrapper object. Two different things.
>
> A disengaged wrapper holds no object at all. Whether that wrapper value is copyable or not depends on the wrapper. But you cannot get a reference to the internal object; this is what distinguishes engagement from disengagement.
The current variant with monostate doesn't design a disengaged variant.
monostate is not a special type.
eggs::variant<Ts...> supports disengaged variants and IIRC I agree that
you can not get a reference to the internal object.
Let me refer to a nullable variant optionals<Ts...>.
This is also the case for optional<T>. We need to make a difference
between sum types (that don't support disengagement and possibly valued
types that support it). As shared pointer have nullptr_t, optional<T>
has nullopt_t, if discriminated_union is a nullable type
>
> Copying a disengaged wrapper does not imply copying a void value.
Agreed.
>
>> 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.
Only nullable types support it.
> Should the user be able to define whether a given type implements engagement or disengagement?
I believed that the committee is not supporting policies.
> Perhaps exception types are all disengaged states?
>
>> 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:
I agree, nullable types should have an associated null_t, as nullopt_t.
This make easier the visitation when there is no engagement.

> Attempting to get something that doesn’t exist is signaled by throwing an exception. This looks like a job for bad_variant_access.
>
> What can you do with null_t that’s generically uniform with things that represent actual values? Once you’ve returned null_t by value, now what? Will every user remember to support null_t in their visitor templates when it’s never mentioned in the type-list?
a nullable type should have associated always a null_t
>
> Use if(v) to avoid visiting disengaged wrappers.
Why you don't want to visit them? a disengaged wrapper has a valid value.
>> 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.
Right, and I believe that we need to make this more homogeneous. I'm for
a null_t for any named none_t. I've not think about std::function. I
need to think about this typeid(void) thing. If we had a nullable
variant (but we don't have it yet) we should have also a nullvar_t.
>> 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?
> The sort order should be implementation-defined; the fact of sorting should not be.
>
>> 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.
> Not for alias templates. They implement many-to-one mapping just fine.
>
>> 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.
>
> If default_initial< int > is defined to be an incomplete type (undefined class), then there’s no inconsistency that variant wouldn’t be able to hold it.
I wouldn't mix the policy tags and the alternative types

std::variant< int, std::string, policy<defaults<int>> >

policy<> can not be a possible alternative

But I don't think the committee is looking for policies based classes.


>> Orthogonality. I covered why this is important in a previous post.
> You’re arguing for non-orthogonality, or uniformity, that a disengaged wrapper is really still holding something.
>
>> 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.
If we can interoperability for nullable sum types we need
optionals<Ts...> with associated null_t nullopt_t.
>
>> 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.
> Eggs.Variant has a disengaged default state expressed by typeid(void), and no distinction of duplicates. It’s even cited by N4542.
>
>> 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.
> variant and any have more than minimal overlap, however you want to classify them.
Agreed.
Vicente

Michael Park

no leída,
22 sept 2015, 3:47:2122/9/15
a ISO C++ Standard - Future Proposals


On Monday, September 21, 2015 at 11:07:18 AM UTC-7, Tony V E wrote:


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, and
       class 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.

Sorry about that, and thanks for the feedback! I had the chosen strategy slightly bluer than others, but it must've been hard to see. I've changed it such that the chosen strategy is blue and the others are grey.
 
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>

All of the above are accurate.

If it has an empty state, what's with the null_t stuff? I think that's where I'm most confused.

The distinction is that every variant has an empty state which is considered valid, but unspecified, but nullability is opt-in with null_t.

That is,

void F(const variant<int, std::string>& v) {
  type_switch (v) (
    [](int) { /* handle int */ },
    [](const std::string&) { /* handle string */ },
  );
}

As opposed to:

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?

It becomes empty even in the presence of null_t. I argue that we don't want to set the value of null_t in the Special Behavior for std::null_t section.
 
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?)

It becomes empty. As proposed, std::null_t gets absolutely no special treatment from variant.
 
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.

Interesting, thanks for mentioning. I'll think about it more.
 
Tony



Anthony Williams

no leída,
22 sept 2015, 4:22:2922/9/15
a std-pr...@isocpp.org
On 22/09/15 04:46, Michael Park wrote:
> I think once you get to a *variant* with duplicate types, you get to a
> point where you don't get any benefit from using a *variant* over a *union*.
>
> Given *variant<int, int>*, the only way to initialize it is with
> index-based constructor: *variant<int, int> v{in_place<0>, 42};*
> and to retrieve the value, index-based get would be used: *get<0>(v)*.

My implementation (https://bitbucket.org/anthonyw/variant) supports
duplicated types with type-based indexing:

se::variant<int,int> v(42);
assert(se::get<int>(v)==42);
assert(v.index()==0);
assert(se::get<0>(v)==42);

se::variant<int,int> v2(se::emplaced_index_t<1>(),42);
assert(v2.index()==1);
assert(se::get<1>(v2)==42);
assert(se::get<int>(v2)==42);

If you have a variant with duplicated types, and you ask for type T then
if the variant contains a type T then you get it. If you care which T it
is, then you need to check the index.

If you put a T in a variant with duplicated types then it will set the
index to the lowest index that is type T. If you care which index value
is used, set it.

> For *variant*to be better than *union*, visitation on such variant must
> be allowed. But how would it be supported?

With my implementation, the same visitor function will be called for
whichever type T is stored.

If you care about which T it is, then you need to check the index, or
tag the types to make them different.

>> 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`.

Agreed. My current implementation has an empty_t when empty, which can
be retrieved/queried/etc. just like any other value. I'm not sure that's
the best plan, I was just exploring.

Larry Evans

no leída,
22 sept 2015, 5:39:3322/9/15
a std-pr...@isocpp.org
On 09/21/2015 10:46 PM, Michael Park wrote:
>
>
[snip]

>
> I think once you get to a *variant* with duplicate types, you get to a
> point where you don't get any benefit from using a *variant* over a
*union*.
>
> Given *variant<int, int>*, the only way to initialize it is with
> index-based constructor: *variant<int, int> v{in_place<0>, 42};*
> and to retrieve the value, index-based get would be used: *get<0>(v)*.
>
> I argue that this is no better than *union { int x; int y; }
> u; *initializing with *u.x = 42;** *and retrieving by u.x.

Except that the variant has the index() function to indicate
which type is actually active, relieving the programmer from
having to remember that.

> I suppose you do get the runtime type-check from *get*, but in terms of
> usability, the user still needs to keep track of which
> member is active.

True, but in the case of union, there no way the user can
query the union to make sure what he believes is the active
member is actually the active member; thus, the proposed
*variant<int,int>* is better than *union{ int x; int y;}*.

>
> Furthermore, while the *name => index* transition seems natural, it's
> not actually the same thing.
> In *union { X x; Y y; } u;* the expression *u.x* refers to the
> same *x* even if we were to re-order the members as *union { Y y; X x;
} u;*
> This is not the case with *variant<X, Y> v;* as the
> expression *get<0>(v)* refers to a different value if the order
> changes: *variant<Y, X>*.
> Note on the other hand, that *get<X>(v)*retrieves the same value
> regardless of the order as desired.

But this mapping of *name => index* can be implemented in a
map-like way, as demonstrated by:

https://gist.github.com/cppljevans/5abee6f5a8deb4f472f9

>
> For *variant*to be better than *union*, visitation on such variant must
> be allowed. But how would it be supported?
> Should the order of the handlers matter?
>
> *variant<int, int> v{in_place<0>, 42};*
> *type_switch (v) (*
> * [](int a) { /* handle first int */ },*
>
> * [](int b) { /* handle second int */ }*
>
> *);*
>
>
> Or should they be required to be tagged with their index?
>
> *variant<int, int> v{in_place<0>, 42};*
>
> *type_switch (v) (*
> * [](tag<int, 0> a) { /* handle first int */ },*
>
> * [](tag<int, 1> b) { /* handle second int */ }*
>
> *);*
>

I think so.

>
> I think this becomes unnecessarily complicated for a problem that would
> be better solved by introducing new types that
> wrap these *int*s which can provide meaningful names
> anyway.

Does this mean that instead of:

* using v_int_int_t= *
* variant *
* < int *
* , int *
* >; *

you would provide wrappers, such as the key_val at:


https://gist.github.com/cppljevans/8e545e8d83946cd74311#file-mapastuple-cpp-L14

and use:

* enum v_indices{v_ndx0,v_ndx1}; *
* using v_int_int_t= *
* variant *
* < key_val<std::integral_constant<v_indices,v_ndx0>,int> *
* , key_val<std::integral_constant<v_indices,v_ndx1>,int> *
* >; *

to assure no duplicate types? Then, to access the v_ndx1
element:

* using v_ndx1_int_t= *
* key_val<std::integral_constant<v_indices,1>,int>; *
* v_int_int_t v_int_int_v= *
* v_ndx1_int_t{}; *
* v_ndx1_int_t v_ndx1_int_v= *
* get<v_ndx1_int_t>(v_int_int_v); *

Is this one example of what you mean by "wrap these *int*s"?

[snip]


Larry Evans

no leída,
22 sept 2015, 5:51:2822/9/15
a std-pr...@isocpp.org
On 09/22/2015 04:39 AM, Larry Evans wrote:
[snip]
>
> Does this mean that instead of:
>
> * using v_int_int_t= *
> * variant *
> * < int *
> * , int *
> * >; *
>
Sorry for those extra *'s. I was trying to emulate the
way Michael highlighted your code, but apparently my
mailer doesn't behave the same as Michael's. Here's
the reformatted code:

using v_int_int_t=
variant
< int
, int
>;
enum v_indices{v_ndx0,v_ndx1};
using v_int_int_t=
variant
< key_val<std::integral_constant<v_indices,v_ndx0>,int>
, key_val<std::integral_constant<v_indices,v_ndx1>,int>
>;
using v_ndx1_int_t=
key_val<std::integral_constant<v_indices,1>,int>;
v_int_int_t v_int_int_v=
v_ndx1_int_t{};
v_ndx1_int_t v_ndx1_int_v=
get<v_ndx1_int_t>(v_int_int_v);


Michael Park

no leída,
22 sept 2015, 6:07:0922/9/15
a ISO C++ Standard - Future Proposals,cpplj...@suddenlink.net
I suppose it's possible to do that, but I meant actually is to introduce new types since (at least as of yet) we don't have strongly-typed type alias. I suppose an somewhat contrived example would be if we represented a student's grade by either a level or a percentage. We can introduce new types Level and Percentage (1) to distinguish the types, (2) to provide meaningful names for each int.

struct Level { int level; };
struct Percentage { int percentage; };

using Grade = variant<Level, Percentage>;

[snip]


Agustín K-ballo Bergé

no leída,
22 sept 2015, 8:17:0622/9/15
a std-pr...@isocpp.org
On 9/22/2015 1:51 AM, David Krauss wrote:
>
>> On 2015–09–22, at 9:49 AM, Agustín K-ballo Bergé
>> <kaba...@hotmail.com <mailto: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.
>
> That’s saying that 80% of it works 100% of the time, or that 100% works
> 80% of the time. The type-based accessor is part of the same class
> interface.

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:

struct foo {};
std::vector<foo> vs, us;
bool eq = (vs == us);

How do you expect that to work? Or are you suggesting that equality
comparison should be removed from `vector` because it has stricter
requirements?

> 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. For a quick
example consider `std::future<R>`, which somehow points to a shared
state that is either empty or contains a result. This result can either
be a value of type `R`, an exception, a function that produces the
value, etc. Simplifying, here's how that is sometimes implemented:

union {
R value;
std::exception_ptr exception;
// ... deferred;
};
int which;

Note, in particular, that `R` could be `std::exception_ptr`.

> Or, let it Just Work™. If selecting the first element is good enough for
> default construction (though I’m not saying it’s so), then selecting the
> first T is good enough for a type-based getter. Simply presume that the
> user is using type-based interface consistently and never indexing.

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.

Agustín K-ballo Bergé

no leída,
22 sept 2015, 8:33:0122/9/15
a std-pr...@isocpp.org
On 9/22/2015 12:46 AM, Michael Park wrote:
> I think once you get to a *variant* with duplicate types, you get to a
> point where you don't get any benefit from using a *variant* over a *union*.
>
> Given *variant<int, int>*, the only way to initialize it is with
> index-based constructor: *variant<int, int> v{in_place<0>, 42};*
> and to retrieve the value, index-based get would be used: *get<0>(v)*.
>
> I argue that this is no better than *union { int x; int y; } u;

You seem to be ignoring the fact that there's a discriminator involved.

Change `int` to `std::string`, and the union version does not give you
copy/move constructors, copy/move assignment operators, not even a
destructor! All these require the discriminator once there's
non-trivially-copyable members in the `union`.

If a discriminated `union` were a first class language construct, then I
would agree there would be no much different to such variant (well, you
get relational operators too, but that's being worked on).

Agustín K-ballo Bergé

no leída,
22 sept 2015, 8:42:1222/9/15
a std-pr...@isocpp.org
On 9/22/2015 9:32 AM, Agustín K-ballo Bergé wrote:
> On 9/22/2015 12:46 AM, Michael Park wrote:
>> I think once you get to a *variant* with duplicate types, you get to a
>> point where you don't get any benefit from using a *variant* over a
>> *union*.
>>
>> Given *variant<int, int>*, the only way to initialize it is with
>> index-based constructor: *variant<int, int> v{in_place<0>, 42};*
>> and to retrieve the value, index-based get would be used: *get<0>(v)*.
>>
>> I argue that this is no better than *union { int x; int y; } u;
>
> You seem to be ignoring the fact that there's a discriminator involved.
>
> Change `int` to `std::string`, and the union version does not give you
> copy/move constructors, copy/move assignment operators, not even a
> destructor! All these require the discriminator once there's
> non-trivially-copyable members in the `union`.

Sorry, that's incorrect. I only care about trivially-copyable members,
the standard actually cares about the triviality of each special member
function.

Nicol Bolas

no leída,
22 sept 2015, 8:52:1522/9/15
a ISO C++ Standard - Future Proposals


On Monday, September 21, 2015 at 12:58:03 PM UTC-4, Matt Calabrese wrote:
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.

I completely understand your feelings here. I think "never empty" was one of the best decisions made by Boost.Variant.

At the same time, I don't think your solution is reasonable. A user has no way of telling if a `variant` is in a state because the user put it there or due to a failure to copy/move. That's bad.

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.

This gives everyone enough freedom to do what they need to do. Those who need to check for copy/move errors can do so. And those who don't, won't. And you don't needlessly gimp `variant`'s interface in some use cases.

David Krauss

no leída,
22 sept 2015, 9:07:3822/9/15
a std-pr...@isocpp.org
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:

std::tuple is never a sum type (at least, in sane usage), so there’s no alternative model that std::get<T> could conceivably follow.

vector::operator== is covariant with its value_type, as are many of its other properties.

The rule for variant type-list duplicates is arbitrary, unrelated to other rules in the language, and unintuitive from the “sum type” perspective — which is solidly grounded in theory, if not C programming tradition.

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. This doesn’t depend on properties of int, unlike your example foo. If the library decides to change and use unsigned as the value of the <0> state, now that’s the cursed type.

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.

Yes. I think I’ll continue to use union for genuine discriminated unions, which usually have constraints to determine the layout and the values of the discriminator.

I’ll be interested when there’s a discriminated_union template that can store the discriminator in a bitfield.

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.

Perhaps variant should not be general enough to implement future (or anyone wishing to do so should just use a strong typedef-alike).

———
Given that a user asking for get<T> just wants a T without caring about any state-machine, the problem is solvable. Stay on the good side of the strict aliasing rule by mapping all discriminator values to the first T in the list. Check get<T> against all matching discriminator values. This makes no behavioral difference except to bless all type-discriminated usage, and the performance should be just fine.

David Krauss

no leída,
22 sept 2015, 9:10:1922/9/15
a std-pr...@isocpp.org

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.

… so if there are any such types going in, you need if(v) before doing any visitation, just as I mentioned.

What percentage of use cases will never see a throwing copy constructor?

Anthony Williams

no leída,
22 sept 2015, 9:17:0222/9/15
a std-pr...@isocpp.org
On 22/09/15 14:07, David Krauss wrote:
> 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. This doesn’t depend
> on properties of int, unlike your example foo. If the library decides to
> change and use unsigned as the value of the <0> state, now that’s the
> cursed type.

That's an implementation issue. My implementation allows duplicates, and
has get<T> always return the contained T.

This seems the best way to handle it to me.

> Given that a user asking for get<T> just wants a T without caring about
> any state-machine, the problem is solvable. Stay on the good side of the
> strict aliasing rule by mapping all discriminator values to the first T
> in the list. Check get<T> against all matching discriminator values.
> This makes no behavioral difference except to bless all
> type-discriminated usage, and the performance should be just fine.

Yes.

Agustín K-ballo Bergé

no leída,
22 sept 2015, 9:31:5322/9/15
a std-pr...@isocpp.org
On 9/22/2015 10:07 AM, David Krauss wrote:
>
>> On 2015–09–22, at 8:17 PM, Agustín K-ballo Bergé
>> <kaba...@hotmail.com <mailto: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:
>
> The rule for variant type-list duplicates is arbitrary, unrelated to
> other rules in the language, and unintuitive from the “sum type”
> perspective — which is solidly grounded in theory, if not C programming
> tradition.

The rules are no different than `get<I>` and `get<T>` for `std::tuple`.
The "sum type" perspective is unrelated, see below.

>>> 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.
>
> Yes. I think I’ll continue to use union for genuine discriminated
> unions, which usually have constraints to determine the layout and the
> values of the discriminator.

At Lenexa, the following vote was taken in order to be able to decide on
the design of `variant`:

Approval vote:
- Discriminated union: 14
- Non-discriminated union (set of types): 3
- Ordered sequence of types, pick first of equals: 8

When considering the current design of the proposed `variant`, keep in
mind that it attempts to model a discriminated union.

If there was a way to sort types at compile time, then a sum type could
be preferred, one where `variant<int, float>` equals `variant<float,
int>`. Note that unlike it was previously said, it would be possible to
do so such that even `std::is_same` would behave as expected:

template <typename Types> class basic_variant { ... };
template <typename ...Ts> using variant =
basic_variant<typename sort<Ts...>::type>;

I believe I would still want some form of lower level
`discriminated_union<Ts...>` alongside such a `variant<Ts...>`, as
that's what I would use to implement the later.

One of the things I regret about Eggs.Variant is not having it called
`eggs::discriminated_union`.

Nicol Bolas

no leída,
22 sept 2015, 10:01:1722/9/15
a ISO C++ Standard - Future Proposals
On Monday, September 21, 2015 at 8:19:29 PM UTC-4, David Krauss wrote:

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.

I am not inventing a need for strong typedefs. We already have such a need. As evidenced by people constantly asking for them.

But in lieu of strong typedefs, we will want some way of putting two distinct typedefs to the same type in a `variant`. That is a reasonable thing for a user to need to do. Especially within generic code.
 
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.

How is this any different from the way people work with `tuple`?
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.

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.
 
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?

My argument is that "disengagement", to the degree possible, should not exist in `variant`s. That's how N4542 works. It doesn't have "disengagement"; it has "invalid". And a `variant` can only become invalid due to a failure of copy/moving of the object it stores.

You can check for validity via the usual expressions. But a key point of N4542 is that a `variant` which contains non-throwing copy/moveable types will never be invalid. So if your `variant` contains only such objects... validity is completely irrelevant to you. You don't have to check it, you don't have to think about it. You just use it normally.

The condition of a `variant` being invalid only exists because `variant`s are value-types and C++ allows copy/moving to fail. It is used only for that, and it should only be tested by code where copy/movement can fail.

Validity is a fundamentally distinct concept from the user saying that a `variant` can have no meaningful value (ie: be empty). That is covered in N4542 via `std::monostate`. It is handled as just another type in the typelist. Just another state.
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.

The more you talk about `function` and `any` in relation to `variant`, the more proof you provide that they are fundamentally different constructs and should have distinct behaviors.

`function` and `any`, by definition, could contain any type (with some restrictions). They can contain types that your code hasn't #included.

`variant`, by definition, doesn't work that way. All of the types it could contain are spelled out explicitly in the typelist, and any code that takes a `variant` must have static access to those type definitions.

Similarly, `function` and `any` both default construct to being empty. `variant` is never empty. It may be invalid, but that's different from being "empty". As such, `variant` default constructs one of its type arguments (N4542 says its the first argument).

Given these distinctions, why would you expect `variant` to use "typeid(void)" to represent being invalid? Just because two other classes, who have completely different behaviors, do so?
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's also the standard for C++ unions. If you find it to be obtuse... so what?

However much more "expressive" your way is, it's also extremely noisy. It makes the type list needlessly bigger.

It also makes it difficult to use generically.
 
It seems like an implementation detail being foisted on the user.

But it's not an implementation detail; it's explicit desired behavior. It's an outgrowth of these design goals:

1: `variant` is never empty.

2: `variant` has a useful default constructor.

That means you have to pick one of the elements from the typelist to construct. And every `variant` must have at least one element, so the first one is a logical choice. It also fits with `union`.
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.

Yes. That's because being "invalid" in N4542 is fundamentally distinct from not having a meaningful value.
 
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.

You're right; I missed that. And it's a good thing to have a specific type that is generally accepted to mean "has no value".
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.

Do you think that your ideas would have led to fewer iterations? No. Because your view of what `variant` should be is not the same as what others want. The Boost people would have been right there, arguing for their ideas of how `variant` should work.

And they would have every right to make that argument. They would have every right to make revisions to your proposals.

The reason for the number of iterations is that different people want different things, and they need to make sure that `variant` satisfies those needs.

The number of revisions is not a good way to judge the quality of a process.
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.

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.

TS’es exist to promote rapid, intense review, to speed the process — whatever it really is.

No, TS's exist for two reasons:

1) To allow out-of-band releases of large-ish functionality, without having to wait for a full version of C++.

2) To allow testing of large-ish functionality in actual implementations, to make sure it actually works well before fully standardizing it.

Study groups are for "rapid, intense review, to speed the process".
 
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.

What "more support" are we talking about?

Also, Eggs.Variant... sucks. Even if you like "always check for empty before using your variant", the fact is it has no visitation.

Visitation is like 90% of why you shouldn't roll your own variant class. Visitation allows you to create visitor types which will fail to compile if you don't cover all of the types in the variant typelist. That solves one of the most pernicious problems when using `variant`s: how to know if someone's added a new type. Multi-visitation is also an extremely useful feature.

Indeed, I would go so far as to say that manually using `get` or whatever to access a `variant` should be considered a code smell, if not a full-on anti-pattern. That's one of the reasons why I consider `variant` and `any` to be wholly distinct types.

So Eggs.Variant is at best incomplete.

Matthew Woehlke

no leída,
22 sept 2015, 10:15:2622/9/15
a std-pr...@isocpp.org
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*.

Strong typedefs might be helpful here; that way the "types" in a variant
could be properly named, i.e. variant<A, B, C> (for meaningful names, of
course), even if all of A, B and C are "really" int.

OTOH I could see it being useful to have a union that has run-time
checking that you use it "correctly", but I'm not sure that can be done
via a purely library implementation; not without some performance
penalty anyway.

--
Matthew

Nicol Bolas

no leída,
22 sept 2015, 13:05:5022/9/15
a ISO C++ Standard - Future Proposals,mwoehlk...@gmail.com
On Tuesday, September 22, 2015 at 10:15:26 AM UTC-4, Matthew Woehlke wrote:
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*.

It's all a matter of how you look at things.

Tuples are conceptually analogized to structs. And the tuple equivalent of a member name is an index.

Therefore, if tuples are to structs as variants are to unions, then it makes sense to be able to use numeric indices in `variant`. That's the equivalent of a union member's name.

Nevin Liber

no leída,
22 sept 2015, 13:06:4522/9/15
a std-pr...@isocpp.org
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
  • Not requiring O(n^2) template instantiations to eliminate it, or
  • Doesn't require brand new compiler magic
unrelated to other rules in the language,

If it is unrelated to other rules in the language, I take it you are against new compiler magic.  That leaves O(n^2) template instantiations.

I don't think significantly slowing down compiles for such a tiny use case is worth it.
 
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. 
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com(847) 691-1404

Matthew Woehlke

no leída,
22 sept 2015, 13:23:0722/9/15
a std-pr...@isocpp.org
On 2015-09-22 13:05, Nicol Bolas wrote:
> On Tuesday, September 22, 2015 at 10:15:26 AM UTC-4, Matthew Woehlke wrote:
>> 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*.
>
> It's all a matter of how you look at things.
>
> Tuples are conceptually analogized to structs. And the tuple equivalent of
> a member name is an index.

That's an... interesting analogy. A tuple is a "generic" struct, i.e. it
has no particular name, the members have no names, and two tuples with
the same layout are compatible (actually, the same type; unlike two
structs with the same layout).

It's not clear to me that the goal of variant is to provide "generic"
unions. Or maybe I should say, it seems that there is also a use case
for something more like a sum type, which would have different
properties (e.g. no duplication of types, type order not meaningful).

--
Matthew

Nevin Liber

no leída,
22 sept 2015, 14:01:2822/9/15
a std-pr...@isocpp.org
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.

But we still have a hard choice between the above alternatives to make, and while there have been almost 1000 email messages on the topic (both here and on internal reflectors) since Lenexa, so far there has been little new additional information added to the discussion to help us make that decision.

Matt Calabrese

no leída,
22 sept 2015, 14:21:3022/9/15
a ISO C++ Standard - Future Proposals
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.

Matt Calabrese

no leída,
22 sept 2015, 14:22:4522/9/15
a ISO C++ Standard - Future Proposals
On Tue, Sep 22, 2015 at 11:21 AM, Matt Calabrese <cala...@google.com> wrote:
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.

Sigh, I need to proofread before I click send. That should read "you can construct that in the event that your variant's move-assignment operator propagates an exception."

Andrew Tomazos

no leída,
22 sept 2015, 14:25:2622/9/15
a std-pr...@isocpp.org
On Tue, Sep 22, 2015 at 8:00 PM, 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
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, and have it be the default branch in any switch on the discriminator.  Trying to wallpaper over it is just going to lead to subtle bugs.

Nevin Liber

no leída,
22 sept 2015, 14:28:2522/9/15
a std-pr...@isocpp.org
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.

Like I said, there is no new information.

Nevin Liber

no leída,
22 sept 2015, 14:38:0622/9/15
a std-pr...@isocpp.org
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.  Is

variant<set<int>, unordered_set<int>>

assignable?

This particular question has caused some committee members to try and resurrect the idea that move constructors should never throw.

And it isn't just assignability; it also applies to emplace construction.  In other words, once you set that variant with a specific type you can never change the type it holds.

Tony V E

no leída,
22 sept 2015, 14:44:1222/9/15
a Standard Proposals
On Tue, Sep 22, 2015 at 2:21 PM, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:
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.

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).  So if assignment fails, I restore the previous state.

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.

:-(

Tony



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/.

Matt Calabrese

no leída,
22 sept 2015, 14:45:5822/9/15
a ISO C++ Standard - Future Proposals
On Tue, Sep 22, 2015 at 11:37 AM, Nevin Liber <ne...@eviloverlord.com> wrote:
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.  Is

variant<set<int>, unordered_set<int>> 

assignable?

If any type involved has a move constructor that's not noexcept, then it's not assignable. If a type's move constructor's noexcept specification varies between implementations and you need the type to be move-assignable, then you can just put in an empty type, and your overall variant is consistent between implementations (apart from exception specification).

Andrew Tomazos

no leída,
22 sept 2015, 14:46:2022/9/15
a std-pr...@isocpp.org
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.


Matt Calabrese

no leída,
22 sept 2015, 14:54:4722/9/15
a ISO C++ Standard - Future Proposals
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.

Tony V E

no leída,
22 sept 2015, 14:58:5522/9/15
a Standard Proposals
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.  Throwing moves are rare - not just that their types are rare, but the conditions for throwing are rare - ie typically _totally_ out of memory (ie can't allocate 16 bytes for a sentinel node).  I'm happy to ignore those types and those rare conditions.  Which means if it is only on a move that throws, I can ignore the error state.

P.S. If it is the construction state, you should also add a reset() function, to be able to make it empty at any time.  (Even if I don't like empty-construct, I like proper APIs, so if we can construct that state, we need reset().)

Tony

Nevin Liber

no leída,
22 sept 2015, 14:59:0022/9/15
a std-pr...@isocpp.org
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.

Tony V E

no leída,
22 sept 2015, 15:12:3822/9/15
a Standard Proposals
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, I probably need to use a specific type (blank_t, null_t, error_t, etc) not just Rectangle, as my default constructible, so I can easily find the offending variant.
Or catch my exceptions closer to their source (I'd prefer not).
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.

As for the ...else compile erorr, like Nevin mentioned, this might be too drastic, making variant unusable.  Or usable but not portable.  (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.)

Andrew Tomazos

no leída,
22 sept 2015, 15:22:1622/9/15
a std-pr...@isocpp.org
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.

Tony V E

no leída,
22 sept 2015, 15:29:3522/9/15
a Standard Proposals

On Tue, Sep 22, 2015 at 3:22 PM, Andrew Tomazos <andrew...@gmail.com> wrote:


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.


I don't like that variant<int, long> (or <Foo,Bar>, because I don't write throwing moves) suddenly needs to handle that third case, that would otherwise be guaranteed to never happen.

I can live with the empty state when has T throwing moves (even if rare), but it bothers me that it is also added for "sane" :-) classes.

Tony


Andrew Tomazos

no leída,
22 sept 2015, 15:34:3422/9/15
a std-pr...@isocpp.org
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.  (I hope! :) )

Matt Calabrese

no leída,
22 sept 2015, 16:43:4922/9/15
a ISO C++ Standard - Future Proposals
On Tue, Sep 22, 2015 at 12:12 PM, Tony V E <tvan...@gmail.com> wrote:


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.

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. In general (I.E. even separate from variant), with a copyable type that has a noexcept move, you can easily get the same effect of the strong guarantee by way of a type that only satisfies the basic guarantee. You copy out a backup and then move-assign to restore the backup if an exception propagates somewhere (or you can sometimes do something more specific to the type). Note that this kind of restoration is really only reliable if the type in question has a noexcept move anyway. 

  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.

Two things -- first, if you have a move-constructor that can throw, that often already means it's because it needs to do dynamic memory allocation, so I don't really buy the avoidance at this level of abstraction. Second, using unique_ptr (or similar indirection solutions) does not necessarily imply dynamic memory allocation at all. It can just be a pointer to some local storage. You can even couple it at a higher level.

Really, though, I just have to recommend that you embrace the basic guarantee. The strong guarantee was never intended as the "default" guarantee to implement. Not that original intent really matters if you truly disagree, but understand that relying on the basic guarantee is not outlandish, but rather it's suggested practice by the original specification of the guarantees. We already have operations in the standard library that only satisfy the basic guarantee, and in my opinion that is a very good thing.

That said, the strong guarantee is simply not implementable without some serious compromises here, and so we shouldn't try to force the type to meet it. If one of the fields has a move constructor that can throw and you have a fallback type, just accept that your variant's copy/move operations only meet the basic guarantee. If all of your types have a move constructor that is noexcept, then your variant's move operations meets the nothrow guarantee and the copy assignment meets the minimum guarantee of that of the corresponding fields. Forcing the strong guarantee punishes those who do not need it and it also complicates the implementation.

Regarding the more general point of the exception guarantees, perhaps a bit subjective but I think is difficult to deny. The guarantee of any operation should be dependent on what is feasible to implement, and not simply on what people want. If we only cared about what people wanted, separate from what is feasible to implement, we would never have an operation that specifies anything less than the strong guarantee (or really the nothrow guarantee, for that matter, if we're talking about ideals here), but that's simply not realistic for many types and operations, and it's really not feasible here. That's okay and also nothing particularly unique to a variant. Not all types in the standard library have operations that meet the strong guarantee, and this would just happen to be one of those types.

Also, apart from all of this, something very important to realize is that if you have an "invalid" state, then you still are only meeting the basic guarantee, so this particular point does not differ! The solution I suggest just gets you there without introducing an unnecessary and separate kind of state.

 
  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

How is this different from dealing with exceptions for any type that encapsulates instances of other types? AFAICT, it's not unique to variant.
 
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.

The problem with an "invalid" state is that it weakens the types invariants and either necessitates adding preconditions to all (or most) other functions, such as visitation, that previously didn't have to be there, or it requires special handling for those operations. For instance, what happens if you visit something in the invalid state? Is this UB? If you don't want it UB, does the visitation result in receiving a tag representing this invalid state, such that people can handle it? If the latter, the user could/should have handled it earlier. If they want to handle it during visitation, they could have, in my suggested solution, just put in some dummy type. As well, if you allow visitation of an invalid state, it means that all visitors need to have an overload that handles the state (unless it is suggested that by default, if such a case is encountered and there is no overload, then you have UB). All of this is just unnecessarily hairy for the specification, for the implementation, and for the user. Avoiding the empty state is simpler to specify, works perfectly and efficiently in the case that you have types with noexcept-move (which in practice, I've personally found, is the common case), and gives more control to the user in the odd case that they are dealing with a type that has a move constructor that can throw. They get a compile error if they try to use the copy-assignment/move-assignment in the case that it is not implemented due to the conditions specified earlier, and if they reach those conditions but never use copy-assignment/move-assignment, things just work anyway. Also understand that copy-construction and move-construction still work, so this type is still very usable in many cases, as it can be passed to and returned from functions, etc.. Finally, this approach prevents the library from making a trade-off decision for users. Instead, the user gets to decide what is best for their application. I.E. If they want something equivalent to an invalid state, they can put it into their variant as a field themselves. 

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.

I don't understand why this would make it unusable. We shouldn't try to define an operation that is not feasible to implement. There are solutions described that will satisfy the user's needs and that does not require making additions/sacrifices to variants state and invariants.

 
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.)

That to me sounds like either A) a poor specification of a type that has a noexcept specification in some implementation but not in others or B) an initial mistake on the part of the user to not put a blank type in there to begin with.

There also is one more solution to consider, that is of a similar ilk. While the variant itself wouldn't be move-assignable or copy-assignable due to the move-constructor being able to throw, an optional of such a variant would, in fact, be able to be move-assignable or copy-assignable, though this may not be immediately obvious. This is, perhaps, even more analogous to the "invalid" state that was suggested, although is again more explicit for the user and they only get it if they explicitly ask for it. It has similar properties to the "invalid" state version in that by default you get such an empty state and also that visitation of the underlying variant does not forward the "invalid" state during visitation and there is also no precondition for visitation regarding some hypothetical invalid state. This is because the "invalid" state is represented at a higher level of abstraction, separate from the variant itself. The underlying variant still has the never empty guarantee. So everything works perfectly fine and variant remains simple. An intelligent implementation can even share storage for the discriminator between the optional and the variant, especially if the optional is implemented on top of variant and there is a more general optimization of that kind already present.

Nicol Bolas

no leída,
22 sept 2015, 17:48:4322/9/15
a ISO C++ Standard - Future Proposals

Why does "consensus" mean "variants can be empty?" It seems to me that they were able to get a good 90% consensus on N4542. It's only been commentary since then that has reopened the issue.

Here's the thing I've never understood about the whole emptiness thing.

If I have need of a variant, and my use case is of the sort where that variant may be empty, N4542 lets me do that by explicitly providing an "empty" state, via the `monostate` type. If I want default constructing the variant to create an empty one, I just make it the first argument in the typelist.

I can even create a specific typedef for it:

template<typename ...Args>
using empty_variant = variant<monostate, Args...>;

However, if my use case for variant is such that it won't be empty... I still have to cover the empty case. Even though I know that it cannot be empty. If I use visitation, I have to handle the empty state. A state that no variant will ever be in.

It seems to me that N4542 is the option that offers the most freedom of choice to the user. Users who want empty variants have them, and users who don't want empty variants don't have to have them.

Why do people want to dictate how I code?

Matt Calabrese

no leída,
22 sept 2015, 17:58:3522/9/15
a ISO C++ Standard - Future Proposals
On Tue, Sep 22, 2015 at 2:48 PM, Nicol Bolas <jmck...@gmail.com> wrote:
Why do people want to dictate how I code?

+1

I'd restate this in the following manner -- why do people want to have a library that specifically forces a trade-off when it remaining agnostic is both simpler and gives more control to the user to make the best decision for their particular case? If there were a solution that was always best, then there wouldn't be an issue, but it really is a trade-off. Because of that, it is the responsibility of a good library author to allow users to be the one to handle that trade-off.

Matt Calabrese

no leída,
22 sept 2015, 18:23:4822/9/15
a ISO C++ Standard - Future Proposals
On Tue, Sep 22, 2015 at 1:43 PM, Matt Calabrese <cala...@google.com> 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. 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 want to reiterate this because I think it might be the most important point of the discussion and may get lost in the mix. Even for types with move constructors that can throw (I.E. something like a list), the case of it throwing is often more theoretical than practical, as in most people will never ever see it happen. However, even though they'd likely never see it happen, they'd always pay for something like double storage if that were what was used to meet the strong guarantee, as has been advocated. This can potentially have huge ramifications for an entire program in the non-exceptional case, just so that if/when move-construction really does propagate an exception, it *might* be easier to handle in some situations, depending on what users need to do for that particular case. This seems like a really questionable rationale, to me at least. What makes variant so much more unique in that the basic guarantee isn't acceptable here? It sounds like the argument is that the strong guarantee is easier to deal with, which I agree is always the case, but that alone is not a complete rationale and effectively rules out ever having operations that meet the basic guarantee. I don't buy it.

Andrew Tomazos

no leída,
22 sept 2015, 18:37:3422/9/15
a std-pr...@isocpp.org
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.  It should default construct to it, it should be queryable via the usual interface and should be mutatable into it.

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).

Matt Calabrese

no leída,
22 sept 2015, 19:04:0122/9/15
a ISO C++ Standard - Future Proposals
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.

Nicol Bolas

no leída,
22 sept 2015, 19:20:3822/9/15
a ISO C++ Standard - Future Proposals

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. And there is nothing specifically about variant that makes copy/move assignment somehow wrong or unacceptable. Even when the copy/move may fail.

Again, why are you dictating who can and cannot use the type? Why are you disfavoring people with throwing copy/move assignments?

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. They would have to roll their own variant type, rather than using the standard library's one.

All because of an unfortunate and unnecessary design decision.

Nevin Liber

no leída,
22 sept 2015, 19:35:4422/9/15
a std-pr...@isocpp.org
On 22 September 2015 at 17:37, Andrew Tomazos <andrew...@gmail.com> wrote:
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.

The current design of N4542 for default construction is also shared by a lot of people.

Nevin Liber

no leída,
22 sept 2015, 19:37:1522/9/15
a std-pr...@isocpp.org
On 22 September 2015 at 18:03, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:

I don't think we should be so quick to jump to an invalid state.

What on earth makes you think we have done so quickly??

The engineering problems don't go away just because some people on mailing lists wish them to.

Nevin Liber

no leída,
22 sept 2015, 19:39:0222/9/15
a std-pr...@isocpp.org
On 22 September 2015 at 18:20, Nicol Bolas <jmck...@gmail.com> wrote:
But that's not what we'd be "accepting" if we accepted your idea. We'd be accepting that some people cannot use variant.

A variant that cannot change the type it holds would be very bad, IMNSHO.  If my answer to people is "well, just use Boost.Variant", then I see no reason to put this one in the standard.

Nicol Bolas

no leída,
22 sept 2015, 19:39:4422/9/15
a ISO C++ Standard - Future Proposals

There are two problems with this.

The first problem is really simple. Let's say I have a variant, and I need to put types into it that can throw on move. And I catch an exception, resolve something, and move on. I inspect the variant, and I find that it is empty.

Now answer me this: is that variant empty because it was set to the empty state or because an exception was thrown?

The answer, of course, is that you don't know.

With N4542, you do know. Because there is a fundamental distinction between the variant being invalid (due to copy/move failure), and the variant containing the `monostate` type (the way you set it to being "empty").

So I would say that your notion that "invalid" and "empty" are the same thing is just wrong.

The second problem is this:

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 isn't impossible; it's impractical. The only two alternatives are to either forbid copy/move if one of the types is not noexcept, or to make copy/move expensive for types that are noexcept.

Also, you don't seem to realize that N4542 gives you never-empty variants. It guarantees never-empty variants... so long as you follow the rules. If all the types you provide are no-except-moveable, then it is exactly equivalent to a "never empty" variant.

It's really the best of all possible worlds. Users of throwing move objects still get to use variant, and do so with no loss of performance on copy/move. They have a way to test to see if the variant is valid, so that they can tell the difference between "empty due to failure" and "empty because I wanted it empty".

And if you don't use throwing move objects... it's a never-empty variant, just like Boost.Variant.

What more could you possibly ask for?

Nevin Liber

no leída,
22 sept 2015, 19:44:3722/9/15
a std-pr...@isocpp.org
On 22 September 2015 at 14:34, Andrew Tomazos <andrew...@gmail.com> wrote:
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. 

Do you plan on softening your stance? :-)
 
It was just a little early at Lenexa and the group was too small. 

We had plenty of email on variant before Lenexa (I'd go back and count, but it's too depressing), but that certainly wasn't enough to get more people into the room to discuss it.
 
I think the 1000+ messages are a sign that people really want std::variant, so that will motivate consensus in the long run.

Some people now believe it isn't worth the amount of time we've spent on it and we should no longer try.

Other people want it to go directly into C++17 (as opposed to a TS), which is more likely to harden, not soften the stance of the people who don't like whatever gets decided in Kona, since we'll never get a chance to correct it once C++17 ships.

Matt Calabrese

no leída,
22 sept 2015, 19:51:2822/9/15
a ISO C++ Standard - Future Proposals
On Tue, Sep 22, 2015 at 4:20 PM, Nicol Bolas <jmck...@gmail.com> wrote:
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.

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. The former is much more acceptable, imo, and sacrifices no overall behavior due to the alternative solutions that I've already suggested.

On Tue, Sep 22, 2015 at 4:20 PM, Nicol Bolas <jmck...@gmail.com> wrote:
Copy assignment and move assignment is not optional for some people. For entirely reasonable people.

Again, they can still get those operations. All they do in that case is simply put in an empty type, or make it optional.
 
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.

There most certainly is. People denying that are just choosing to not accept the issue. Again, operations existing on a type being predicated by properties of template arguments is nothing special here. The only thing different is that the predicate in this case depends on an exception specification instead of an operation just existing.
 
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?

We're not! This all works in such cases. All we are doing is not implementing what is not implementable and properly identifying the predicate that makes it so. If you want the type copy-assignable or move-assignable for your variant that has fields with throwing move constructors, you can get that by just putting in a dummy type along side the other fields. There is no sacrificed functionality here, all it does is avoid introducing an unnecessary notion of an invalid state and complicating specification/uses.

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.

It doesn't favor one or the other, it just predicates the operations appropriately. You can still use variant in those cases even if you need those operations and you don't already meet the predicate because satisfying the predicate only requires putting in some empty type or by making the type optional (both of these are equivalent to variant having an invalid state, with which one being "more" equivalent depending on if you want to handle visitation of an invalid state as UB or as receiving a dummy type). All that's changed is that in my suggested approach, the invariants of the type are not violate, and people need to be explicit if they are to get such a state.

Nevin Liber

no leída,
22 sept 2015, 19:53:4522/9/15
a std-pr...@isocpp.org
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.

Nevin Liber

no leída,
22 sept 2015, 19:59:0922/9/15
a std-pr...@isocpp.org
On 22 September 2015 at 18:51, 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:
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 isn't out of necessity.  It results from over-constraining the problem, by not allowing double buffering, an invalid state or termination.
 
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.

Or use double buffering.

David Krauss

no leída,
22 sept 2015, 20:21:5722/9/15
a std-pr...@isocpp.org

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

Arbitrary as in unteachable and illogical compared to other rules which wouldn’t break the program.

I guess you got O(n^2) time from the idea of sorting the list. Some other alternatives are:

1. Use the first T found, with early exit. This can only be faster.
2. As Michael and I mentioned, do not use duplicate T’s for storage, but preserve duplicates as indexes only. This still takes O(n) instantiations.
3. Disallow all duplicates.
4. No index-based get. This is what corresponds to enabling sorting once the compiler magic is available, but it doesn’t seem to be on the table for now.

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.

Those are the people for which alternative #1 would fail, but that’s not what you’re advocating. You’re advocating compile-time failure for users who use type-based indexing.

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. 

No, it can’t. To (meaningfully) put a discriminator in a bitfield, it needs to be in the common initial subsequence of each of the variant members. Doing so, it can be stored without taking a byte, obviously.

std::variant uses so many defaults and shortcuts, just due to being a template, that it can’t compete with the actual C unions it models. What’s its real-world use case? What advantage besides saying “Look ma, I’m using templates!”

The union keyword is not deprecated. We have unions already. We don’t have sum types, so that’s the solution I anticipated more.

Matt Calabrese

no leída,
22 sept 2015, 20:29:5722/9/15
a ISO C++ Standard - Future Proposals
On Tue, Sep 22, 2015 at 4:53 PM, Nevin Liber <ne...@eviloverlord.com> wrote:
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.

I would hesitate to refer to doubling the storage space of a type by way of the word "only." Depending on your situation that could be pretty huge, especially if you have a lot of variants, and even more so if variants happen to contain other variants. If you just don't specify the strong guarantee, which plenty of types already do for many operations, including some in the standard library, then this need goes away. It has never been recommended practice to force types to meet the strong guarantee when they don't naturally fit. variant does not need to handle this and there are usually alternative solutions that the user can use at their level of abstraction that do not have such drastic ramifications, such as dealing with the offending objects via indirection, which again, does not even imply dynamic memory allocation, not that it likely matters given that move operations that can throw generally are that way because they are doing dynamic memory allocation to begin with.

On Tue, Sep 22, 2015 at 4:53 PM, Nevin Liber <ne...@eviloverlord.com> wrote:
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.

I don't consider it problematic, it just tells us that specifying the strong guarantee would be a mistake here. The basic guarantee has been the recommended default since the guarantees were first introduced. What makes people think that it's particularly important here? Especially given that exceptions *actually* being thrown from types that have move-constructors that can throw is [likely] rare (this is a generalization that is not always valid, but I suspect most people will agree with), it seems really questionable to put a requirement on the implementation to effectively double the storage for every instance. You are doubling the storage required for your variants just to make an exceptional case (that many applications will likely never even see) a little bit easier to deal with. That's a really questionable trade-off to me. At the very least, you have to admit it's understandably controversial.
Está cargando más mensajes.
0 mensajes nuevos