std::variant, overload resolution

177 views
Skip to first unread message

chris beck

unread,
Jul 26, 2016, 1:51:20 PM7/26/16
to std-pr...@isocpp.org
Greetings,

This is not actually a proposal, rather, a question about another proposal. My belief is that this is on-topic -- If this is the wrong place to ask this question, sorry.

---------------

Variants have become a hot topic in modern C++ in recent years. Beginning with `boost::variant` and leading into the `std::variant` proposal, variant has become a part of the modern C++ lexicon.

Much has been written about variants, particularly about the "never-empty" guarantee, and the costs of this guarantee. For instance, in this article (https://isocpp.org/blog/2015/11/the-variant-saga-a-happy-ending), the author states:

> There are other design decisions – default construction, visitation etc – but they are all insignificant compared to how to deal with the throwing, type-changing assignment.

What I would like to ask about is a different issue, which is implicit conversions.

One thing that was slightly annoying to me about boost::variant was that things like this would compile:

```
    boost::variant<std::string, int> v;

    v = true
```

and types like `boost::variant<int, float>` could often lead to ambiguous overload resolution when making assignments to them.

In one project, I wanted to use `boost::variant` to represent a value from an interface with a scripting language, which naturally would have been `boost::variant<bool, int, float, std::string, ...>`. But I found it more complicated than I would like to figure out exactly what the variant was doing when I made assignments to it. Probably because I'm still a novice and I generally understand overload resolution from experience rather than from a close reading of the standard.

What I ended up doing in that project instead was, I made a different variant type in which overload resolution is not used, and instead it uses an iterative strategy. I made a type trait that eliminates undesirable integral conversions, and prevents conversions between integral, floating point, pointer type, character type, boolean, and wchar_t, and prevents narrowing conversions. (I won't describe my trait in full detail.)

There are a few drawbacks to a variant like this that I can see.

- With the overload-resolution-based implementation when no match is found, the compiler will give you list of reasons in each case why the overload was rejected. When you do iteration via TMP, when you get to the end of the list the only real error you can give is "no match found". I suppose you could try to use some really fancy TMP that will accumulate a list of errors and put that info in the static assert, but I didn't attempt that.

- Overload resolution is a core language feature and everyone already knows how it works more or less. So even if it doesn't always do what they want, people will be more comfortable with a variant based on overload resolution and it will feel more familiar to them.

I guess what I'd like to ask is, since the designers of std::variant thought that this issue was much less important than the "never empty" guarantee, can anyone give some insight into how they view this issue? I used my no-overload resolution version in another project for a while now and I didn't experience any major usability issues. But maybe there are more subtle issues I didn't think about, or advantages of the overload-resolution strategy that I didn't know of.

Would be very interested to hear peoples' opinions.

Best Regards,
Chris Beck

Nicol Bolas

unread,
Jul 26, 2016, 4:58:23 PM7/26/16
to ISO C++ Standard - Future Proposals, bec...@gmail.com

Well, the alternatives are:

1) Use overload resolution.

2) Use only an exact match.

3) Make up arbitrary rules.

As you point out, #1's rules may be esoteric and unhelpful in some places, but they are consistent. So you don't have to keep multiple sets of rules in mind.

#2 was actually tried in the first proposal: N4218. The constructor from a `T&&` was explicit and it only worked if `T` were an exact match for a type. This state of affairs lasted until P0088R1, which the current overload-based form was introduced. However, at no point was it explained why this change was adopted.

So in terms of insight, what we can easily determine is that the committee preferred #1 over #2. No word on if they considered some from of #3.

Personally, I don't like #3 very much. While I'm against narrowing conversions, I don't think it's worth it to force arbitrary rules on users in this way. Even if those rules can be argued to be better than the existing overload rules, it still is inconsistent.

Also, it's something you can fix yourself. It wouldn't be hard at all to write a template function to construct a `std::variant` using your desired rules. If you wanted, you could build a whole type around this, with `variant` being used internally to do the hard work.

chris beck

unread,
Jul 26, 2016, 6:45:00 PM7/26/16
to Nicol Bolas, ISO C++ Standard - Future Proposals
That's a really good point, thanks.

Chris Beck

Tony V E

unread,
Jul 26, 2016, 7:14:23 PM7/26/16
to Standard Proposals
On Tue, Jul 26, 2016 at 4:58 PM, Nicol Bolas <jmck...@gmail.com> wrote:
On Tuesday, July 26, 2016 at 1:51:20 PM UTC-4, chris beck wrote:
Greetings,

This is not actually a proposal, rather, a question about another proposal. My belief is that this is on-topic -- If this is the wrong place to ask this question, sorry.

---------------

Variants have become a hot topic in modern C++ in recent years. Beginning with `boost::variant` and leading into the `std::variant` proposal, variant has become a part of the modern C++ lexicon.

Much has been written about variants, particularly about the "never-empty" guarantee, and the costs of this guarantee. For instance, in this article (https://isocpp.org/blog/2015/11/the-variant-saga-a-happy-ending), the author states:

> There are other design decisions – default construction, visitation etc – but they are all insignificant compared to how to deal with the throwing, type-changing assignment.

What I would like to ask about is a different issue, which is implicit conversions.

One thing that was slightly annoying to me about boost::variant was that things like this would compile:

```
    boost::variant<std::string, int> v;

    v = true
```

and types like `boost::variant<int, float>` could often lead to ambiguous overload resolution when making assignments to them.



...
 

Well, the alternatives are:

1) Use overload resolution.

2) Use only an exact match.

3) Make up arbitrary rules.

As you point out, #1's rules may be esoteric and unhelpful in some places, but they are consistent. So you don't have to keep multiple sets of rules in mind.

#2 was actually tried in the first proposal: N4218. The constructor from a `T&&` was explicit and it only worked if `T` were an exact match for a type. This state of affairs lasted until P0088R1, which the current overload-based form was introduced. However, at no point was it explained why this change was adopted.

So in terms of insight, what we can easily determine is that the committee preferred #1 over #2. No word on if they considered some from of #3.


I can't recall exactly when this changed in the committee, but I suspect everyone wanted this to work:

    std::variant<std::string, int> vsi = "Hello world";

And also, variant<int> should work much like int.

If someone doesn't like some of the conversions (mostly inherited from C), and I don't blame you if you do, {} construction usually lessens those.  So maybe

    std::variant<int> vi = 17.2f;
vs
    std::variant<int> vi2{17.2f};

I don't actually know which works, I can't find an online compiler with variant. :-(

Edward Catmur

unread,
Jul 26, 2016, 9:20:34 PM7/26/16
to ISO C++ Standard - Future Proposals, bec...@gmail.com
On Tuesday, 26 July 2016 18:51:20 UTC+1, chris beck wrote:
What I ended up doing in that project instead was, I made a different variant type in which overload resolution is not used, and instead it uses an iterative strategy. I made a type trait that eliminates undesirable integral conversions, and prevents conversions between integral, floating point, pointer type, character type, boolean, and wchar_t, and prevents narrowing conversions. (I won't describe my trait in full detail.)

There are a few drawbacks to a variant like this that I can see.

- With the overload-resolution-based implementation when no match is found, the compiler will give you list of reasons in each case why the overload was rejected. When you do iteration via TMP, when you get to the end of the list the only real error you can give is "no match found". I suppose you could try to use some really fancy TMP that will accumulate a list of errors and put that info in the static assert, but I didn't attempt that. 

That sounds like a potential disaster; it's easy to conceive of situations where the selected type list member would depend on ordering of the type list i.e. MyVariant<A, B, C>{x} would have a different stored type to MyVariant<C, B, A>{x}. It would be better to generate a constructor template for each member of the type list and constrain by your conversion predicate; this would (with a modern compiler) give sensible errors both for the no overload and ambiguous overload cases. Admittedly, you'd have to ensure that exact matches with one of the types were preferred to conversions to another type, but you could do that with tag dispatch. As others have said, this would be reinventing overload resolution with slightly different, unfamiliar and possibly less battle-hardened rules.

Arthur O'Dwyer

unread,
Jul 27, 2016, 12:17:20 AM7/27/16
to ISO C++ Standard - Future Proposals
On Tuesday, July 26, 2016 at 4:14:23 PM UTC-7, Tony V E wrote:
On Tue, Jul 26, 2016 at 4:58 PM, Nicol Bolas <jmck...@gmail.com> wrote:

Well, the alternatives are:

1) Use overload resolution.
2) Use only an exact match.
3) Make up arbitrary rules.

As you point out, #1's rules may be esoteric and unhelpful in some places, but they are consistent. So you don't have to keep multiple sets of rules in mind.

#2 was actually tried in the first proposal: N4218. The constructor from a `T&&` was explicit and it only worked if `T` were an exact match for a type. This state of affairs lasted until P0088R1, which the current overload-based form was introduced. However, at no point was it explained why this change was adopted.

So in terms of insight, what we can easily determine is that the committee preferred #1 over #2. No word on if they considered some from of #3.

I can't recall exactly when this changed in the committee, but I suspect everyone wanted this to work:

    std::variant<std::string, int> vsi = "Hello world";

I was in the room (LEWG) at Kona and I recall that exact example coming up.
In fact I'm pretty sure you were also in the room, Tony. :)

I don't remember whether the debate was between #1 and #2 or between #1 and #3; I actually suspect that it was between #1 and #3, in that even the proposal's originators believed that #2 was unworkable.
...In fact, I went and dug up a couple versions of the proposal. N4450 (v2) implements exactly #1, as far as I can tell.
By the time we get to P0088R1 (v6), the revision history shows that the Kona discussion I remember was actually where LEWG reversed its earlier consensus on the undesirability of implicit conversions —

Results of LEWG review in Lenexa:
    • Remove conversions, e.g. variant<int, string> x = "abc";? SF=5 WF=4 N=1 WA=1 SA=0
 Results of LEWG review in Kona:
    • Allow conversion in both construction and assignment. The detailed polls were:
      • keep assignment and construction asymmetrical: SF=0, F=0, N=1, A=7, SA=6
      • restrict assign and construction to alternative types only: SF=2, F=5, N=4, A=3, SA=4
      • allow conversion for construction and assignment: SF=4, F=4, N=3, A=4, SA=0
HTH,
–Arthur

chris beck

unread,
Jul 27, 2016, 7:43:46 AM7/27/16
to Edward Catmur, ISO C++ Standard - Future Proposals
> it's easy to conceive of situations where the selected type list member would depend on ordering of the type list

i guess i was viewing that as a feature, not a bug. The idea was that the list establishes a priority, so you can put your "smallest" types first and they will be prioritized in event of a match.


> Admittedly, you'd have to ensure that exact matches with one of the types were preferred to conversions to another type, but you could do that with tag dispatch.

That's interesting, i guess I don't see how to do that right now, but I will think about it.


> As others have said, this would be reinventing overload resolution with slightly different, unfamiliar and possibly less battle-hardened rules.

Yeah, but at least I don't get implicit conversions. In some use-cases, I think that's worth it. YMMV I guess.

Howard Hinnant

unread,
Jul 27, 2016, 9:53:02 AM7/27/16
to std-pr...@isocpp.org
Fwiw, example implementation of variant as specified in the CD:

https://github.com/efcs/libcxx/blob/variant/include/variant

If you find any difference between the specification and this implementation, speak loudly.

Howard

signature.asc
Reply all
Reply to author
Forward
0 new messages