Why std::optional has checked and unchecked access, but std::variant only checked?

757 views
Skip to first unread message

Oleksandr Pikozh

unread,
Jan 16, 2018, 1:33:53 AM1/16/18
to std-pr...@isocpp.org
In std::optional we have:
- operator* — doesn't check whether an optional contains a value;
- method value — throws std::bad_optional_access when an optional
doesn't contain a value.

In std::vector we have:
- operator[] — doesn't check whether a vector contains specified index;
- method at — throws std::out_of_range when a vector doesn't contain
specified index.

In std::variant we have:
- ??? — doesn't check whether a variant contains specified index/type;
- method get — throws std::bad_variant_access when a variant doesn't
contain specified index/type.

Why we don't have any fast (unchecked) ways to access std::variant?
Assuming context when user has already checked for presence of required
item somehow and now needs the fastest access (i.e. he agrees to get UB
when absent).

Nicol Bolas

unread,
Jan 16, 2018, 10:05:41 AM1/16/18
to ISO C++ Standard - Future Proposals, o.pi...@gmail.com
Because if you didn't want checking, you'd be using a `union`. Also, you're not generally supposed to fetch values from a `variant`; you're supposed to be visiting it and acting on it that way.

Richard Hodges

unread,
Jan 16, 2018, 10:11:26 AM1/16/18
to std-pr...@isocpp.org
Also, you're not generally supposed to fetch values from a `variant`, you're supposed to be visiting it and acting on it that way.

I have wondered before whether optionals should also be visitable, since logically, they could be thought of as a variant of T and nullopt_t


On 16 January 2018 at 16:05, Nicol Bolas <jmck...@gmail.com> wrote:
Because if you didn't want checking, you'd be using a `union`. Also, you're not generally supposed to fetch values from a `variant`; you're supposed to be visiting it and acting on it that way.

--
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-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/5dd620cd-a8ef-4354-a14c-6992421729ac%40isocpp.org.

Zhihao Yuan

unread,
Jan 16, 2018, 10:38:58 AM1/16/18
to std-pr...@isocpp.org
While it's not a bad idea (pattern matching in other
languages naturally support this use), in practise


may be easier to use.

--
Zhihao Yuan, ID lichray
The best way to predict the future is to invent it.
_______________________________________________


-------- Original Message --------
UTC Time: January 16, 2018 3:11 PM

Oleksandr Pikozh

unread,
Jan 16, 2018, 10:53:38 AM1/16/18
to ISO C++ Standard - Future Proposals, Nicol Bolas
Well, an object is often used in different ways from different parts of code: more carefree from relatively-rarely-called routines and more optimized from relatively-often-called routines. Therefore it's good for the same tool to support different modes; sometimes I want checking, sometimes I don't (i.e. I want std::variant that keeps its `index`, but sometimes to allow to do the `get`-like thing without internal checks); as for me, that's the main goal of C++ — to support higher-level programming without losing the low-level control. Absence of unchecked operations (comparable to std::optional::operator* and std::vector::operator[]) makes std::variant (almost) useless for me: it looks more psychologically-comfortable to use "enum state + union value" from the beginning, than to integrate std::variant with feeling that it implies checks that I can never get rid of (even in cases when inner type is guaranteed by outer logic).

Besides that the same your argument can be applied for std::optional too — "if you didn't want checking, you'd be using a `T value` instead of `std::optional<T> value`", right? (But no, we still have checkless `std::optional::operator*` — because there is no need to check again in `if (o) {…; f(*o); …;}`-like cases, for example.)

"you're supposed to be visiting it and acting on it that way" — am I supposed to act _only_ in that way?

Nicol Bolas

unread,
Jan 16, 2018, 11:35:10 AM1/16/18
to ISO C++ Standard - Future Proposals, jmck...@gmail.com, o.pi...@gmail.com
On Tuesday, January 16, 2018 at 10:53:38 AM UTC-5, Oleksandr Pikozh wrote:
Well, an object is often used in different ways from different parts of code: more carefree from relatively-rarely-called routines and more optimized from relatively-often-called routines. Therefore it's good for the same tool to support different modes; sometimes I want checking, sometimes I don't (i.e. I want std::variant that keeps its `index`, but sometimes to allow to do the `get`-like thing without internal checks); as for me, that's the main goal of C++ — to support higher-level programming without losing the low-level control. Absence of unchecked operations (comparable to std::optional::operator* and std::vector::operator[]) makes std::variant (almost) useless for me: it looks more psychologically-comfortable to use "enum state + union value" from the beginning, than to integrate std::variant with feeling that it implies checks that I can never get rid of (even in cases when inner type is guaranteed by outer logic).

Besides that the same your argument can be applied for std::optional too — "if you didn't want checking, you'd be using a `T value` instead of `std::optional<T> value`", right? (But no, we still have checkless `std::optional::operator*` — because there is no need to check again in `if (o) {…; f(*o); …;}`-like cases, for example.)

"you're supposed to be visiting it and acting on it that way" — am I supposed to act _only_ in that way?


I generally consider the use of `get` on a variant at all to be a code-smell. A function which takes a variant is a function that is expected to be able to operate correctly on all of the possible values in the variant. And the most effective way to achieve that is to use visitation when you want to access the member. By using `get`, you're saying that the code in question simply can't handle the other possible states of the variant. That's the wrong way to use a variant.

Visitation fixes so many problems. It makes it statically impossible for you to forget to handle a state. It makes your code more readable (to a degree), since you can use the variant value like it were a regular value. It discourages lots of conditions. And so forth.

That's not to say that `get` is bad. It's simply not a tool that should be in such frequent use that not having an unchecked version is a legitimate problem.

Consider your example of "when inner type is guaranteed by outer logic". How did this "outer logic" provide this guarantee? Did it create the `variant` with that state? If so... why did it bundle it in a `variant`, when it clearly only provides an object of a specific type? Did the "outer logic" already do the test? If so, why did it not unpack the object directly when it did that test, and just pass the object of the right type? If the "inner" logic can only handle a specific state, why is it being given an object that can assume states it cannot handle? That is, rather than:

if(var.index() == required_index)
{
 
auto &value = get<required_index>(var);
}

You do this:

if(auto ptr = get_if<required_index>(&var); ptr)
{
 
//use `ptr`
}

The fundamental difference between `optional` and `variant` is that the former only ever has 2 states. The behavior for the unengaged state will almost always be "do nothing", while the behavior for the engaged state will be "do something". Your code strongly suggests that you're trying to treat a `variant` like an `optional`. And we have a way to do that: get a pointer to the member.

Also, I'm curious as to your application where a single conditional check is sufficient to make the type "(almost) useless for me". Seriously, is that trivial performance really sufficient to go write your own variant type or to use something less feature-rich?

You're also missing another problem with checked vs. unchecked operations: consistency.

In the C++ standard library, the way you tell whether an operation is checked or unchecked is that the unchecked operation uses an operator. `vector::operator[]`, `optional::operator*`, and so on are operators. If you use a named function, then it is a checked operation. `vector::at`, `optional::value`, and so forth.

You can't really have an operator-form of the `variant` `get` function. After all, you need to provide a template index/type, and you can't pass something like that as a function parameter to an operator. After all, operators cannot take explicit template parameters without calling them by spelling out the operator (`variant::operator[]<std::string>(0)`).

Well... you can work around this, but it would require wrapping it up in a constexpr type, and then having a template extract the type from the parameter. So you'd have to do something like this:

var[type_container<Type>{}];
var[3_const_index];

And allow template argument deduction of `operator[]` to extract the typename or index to access.

Oleksandr Pikozh

unread,
Jan 16, 2018, 1:02:56 PM1/16/18
to ISO C++ Standard - Future Proposals, Nicol Bolas
On 16.01.18 18:35, Nicol Bolas wrote:
On Tuesday, January 16, 2018 at 10:53:38 AM UTC-5, Oleksandr Pikozh wrote:
Well, an object is often used in different ways from different parts of code: more carefree from relatively-rarely-called routines and more optimized from relatively-often-called routines. Therefore it's good for the same tool to support different modes; sometimes I want checking, sometimes I don't (i.e. I want std::variant that keeps its `index`, but sometimes to allow to do the `get`-like thing without internal checks); as for me, that's the main goal of C++ — to support higher-level programming without losing the low-level control. Absence of unchecked operations (comparable to std::optional::operator* and std::vector::operator[]) makes std::variant (almost) useless for me: it looks more psychologically-comfortable to use "enum state + union value" from the beginning, than to integrate std::variant with feeling that it implies checks that I can never get rid of (even in cases when inner type is guaranteed by outer logic).

Besides that the same your argument can be applied for std::optional too — "if you didn't want checking, you'd be using a `T value` instead of `std::optional<T> value`", right? (But no, we still have checkless `std::optional::operator*` — because there is no need to check again in `if (o) {…; f(*o); …;}`-like cases, for example.)

"you're supposed to be visiting it and acting on it that way" — am I supposed to act _only_ in that way?


I generally consider the use of `get` on a variant at all to be a code-smell. A function which takes a variant is a function that is expected to be able to operate correctly on all of the possible values in the variant. And the most effective way to achieve that is to use visitation when you want to access the member. By using `get`, you're saying that the code in question simply can't handle the other possible states of the variant. That's the wrong way to use a variant.

Visitation fixes so many problems. It makes it statically impossible for you to forget to handle a state. It makes your code more readable (to a degree), since you can use the variant value like it were a regular value. It discourages lots of conditions. And so forth.

That's not to say that `get` is bad. It's simply not a tool that should be in such frequent use that not having an unchecked version is a legitimate problem.
Looks like oversimplification for me.
The set of real-life cases is much more rich.
E.g. we can have an std::variant field in a class — and some more-universal methods (that handle all states combinations) in the class can call some more-specialized ones (that handle only specific state combinations). Of course, we can cache references (pointers) to members of std::variant and pass them from method to method (in addition to passing `this`, which already has access to the whole std::variant field), but it decreases aesthetics of the code (and additionally looks redundant at first glance, because with the most expected (but not guaranteed, of course) implementation of variant, the pointer to the member would have fixed offset from the pointer to the whole std::variant).


Consider your example of "when inner type is guaranteed by outer logic". How did this "outer logic" provide this guarantee? Did it create the `variant` with that state? If so... why did it bundle it in a `variant`, when it clearly only provides an object of a specific type? Did the "outer logic" already do the test? If so, why did it not unpack the object directly when it did that test, and just pass the object of the right type? If the "inner" logic can only handle a specific state, why is it being given an object that can assume states it cannot handle? That is, rather than:

if(var.index() == required_index)
{
 
auto &value = get<required_index>(var);
}

You do this:

if(auto ptr = get_if<required_index>(&var); ptr)
{
 
//use `ptr`
}

The fundamental difference between `optional` and `variant` is that the former only ever has 2 states. The behavior for the unengaged state will almost always be "do nothing", while the behavior for the engaged state will be "do something". Your code strongly suggests that you're trying to treat a `variant` like an `optional`. And we have a way to do that: get a pointer to the member.
Yes, I consider variant as a generalization of optional. But (for some reasons that look bogus for me) the generalization isn't smooth. IMHO it is better to fix it (even if no so much people feel practical problems due to that shagginess — just for aesthetic reasons at least).


Also, I'm curious as to your application where a single conditional check is sufficient to make the type "(almost) useless for me". Seriously, is that trivial performance really sufficient to go write your own variant type or to use something less feature-rich?
It's more about consistency and psychology, not about actual benchmarking yet. I am writing right now a piece of code that with high probability will be a part of bottleneck. I realize that in some cases std::variant does redundant checks (at least with the way I use it). I expect them to be avoidable (as in std::optional and others), but they aren't. So it's psychologically simpler for me to throw std::variant out and not to use in the "foundation" (that will probably be a bottleneck) any things that have _known_ overhead. Not necessarily variant::get will be a problem (and most probably something absolutely else will), but I don't like a feeling of creating future problems when writing the code (i.e. to write a code with a known and easy-avoidable redundancy, which doesn't brings and significant improvements into the code aesthetics or some other tremendous benefits).

You're also missing another problem with checked vs. unchecked operations: consistency.

In the C++ standard library, the way you tell whether an operation is checked or unchecked is that the unchecked operation uses an operator. `vector::operator[]`, `optional::operator*`, and so on are operators. If you use a named function, then it is a checked operation. `vector::at`, `optional::value`, and so forth.

You can't really have an operator-form of the `variant` `get` function. After all, you need to provide a template index/type, and you can't pass something like that as a function parameter to an operator. After all, operators cannot take explicit template parameters without calling them by spelling out the operator (`variant::operator[]<std::string>(0)`).
I never heard about "methods-are-checked, functions-are-unchecked" rule in stdlib. Is is stated somewhere?
As for me, much more expected understanding of consistency here would be that every class (where it's applicable) has two ways of access: checked and unchecked. (If the std::vector and std::optional had no unchecked access, then I would have no disturbance about std::variant — I'd just see that as style of C++ stdlib; even if only std::optional had no unchecked access, then it would look understandable for me (stdlib is old, std::vector created long before std::optional and std::variant, style changed since then); but now it looks as breaking its own style.)
And if unchecked things really need to be operators (which I doubt, even if the such rule exists and wasn't broken yet, it will be surely be broken at some point of time in future due to the growing of stdlib and growing of the amount of strongly-needed-to-be-unchecked things), why not simply the explicit cast operator? (Ah, you are right, explicit cast operator will allow only access by type, not by index — still I don't feel both access by index and .)

Tony V E

unread,
Jan 16, 2018, 2:17:43 PM1/16/18
to Standard Proposals
On Tue, Jan 16, 2018 at 11:35 AM, Nicol Bolas <jmck...@gmail.com> wrote:
On Tuesday, January 16, 2018 at 10:53:38 AM UTC-5, Oleksandr Pikozh wrote:
Well, an object is often used in different ways from different parts of code: more carefree from relatively-rarely-called routines and more optimized from relatively-often-called routines. Therefore it's good for the same tool to support different modes; sometimes I want checking, sometimes I don't (i.e. I want std::variant that keeps its `index`, but sometimes to allow to do the `get`-like thing without internal checks); as for me, that's the main goal of C++ — to support higher-level programming without losing the low-level control. Absence of unchecked operations (comparable to std::optional::operator* and std::vector::operator[]) makes std::variant (almost) useless for me: it looks more psychologically-comfortable to use "enum state + union value" from the beginning, than to integrate std::variant with feeling that it implies checks that I can never get rid of (even in cases when inner type is guaranteed by outer logic).

Besides that the same your argument can be applied for std::optional too — "if you didn't want checking, you'd be using a `T value` instead of `std::optional<T> value`", right? (But no, we still have checkless `std::optional::operator*` — because there is no need to check again in `if (o) {…; f(*o); …;}`-like cases, for example.)

"you're supposed to be visiting it and acting on it that way" — am I supposed to act _only_ in that way?


I generally consider the use of `get` on a variant at all to be a code-smell. A function which takes a variant is a function that is expected to be able to operate correctly on all of the possible values in the variant. And the most effective way to achieve that is to use visitation when you want to access the member. By using `get`, you're saying that the code in question simply can't handle the other possible states of the variant. That's the wrong way to use a variant.

Visitation fixes so many problems. It makes it statically impossible for you to forget to handle a state. It makes your code more readable (to a degree), since you can use the variant value like it were a regular value. It discourages lots of conditions. And so forth.

That's not to say that `get` is bad. It's simply not a tool that should be in such frequent use that not having an unchecked version is a legitimate problem.

Consider your example of "when inner type is guaranteed by outer logic". How did this "outer logic" provide this guarantee? Did it create the `variant` with that state? If so... why did it bundle it in a `variant`, when it clearly only provides an object of a specific type? Did the "outer logic" already do the test? If so, why did it not unpack the object directly when it did that test, and just pass the object of the right type? If the "inner" logic can only handle a specific state, why is it being given an object that can assume states it cannot handle? That is, rather than:

if(var.index() == required_index)
{
 
auto &value = get<required_index>(var);
}

You do this:

if(auto ptr = get_if<required_index>(&var); ptr)
{
 
//use `ptr`
}

The fundamental difference between `optional` and `variant` is that the former only ever has 2 states. The behavior for the unengaged state will almost always be "do nothing", while the behavior for the engaged state will be "do something". Your code strongly suggests that you're trying to treat a `variant` like an `optional`. And we have a way to do that: get a pointer to the member.

Also, I'm curious as to your application where a single conditional check is sufficient to make the type "(almost) useless for me". Seriously, is that trivial performance really sufficient to go write your own variant type or to use something less feature-rich?

You're also missing another problem with checked vs. unchecked operations: consistency.

In the C++ standard library, the way you tell whether an operation is checked or unchecked is that the unchecked operation uses an operator. `vector::operator[]`, `optional::operator*`, and so on are operators. If you use a named function, then it is a checked operation. `vector::at`, `optional::value`, and so forth.


The std isn't very consistent, but it does tend to favour the fast over the safe.  It might have been better to make the unsafe operations long/ugly names, but that would have hurt C++ adoption in the early days.

One attempt at consistency is that (due to pointers) operator* is seen as "potentially dangerous", so optional wanted to use the same syntax for the same danger. Great.

Note, however, that variant is using std::get, which was previously noexcept (for tuple), but can throw for variant - different warning signs for the same syntax :-(

And they are fundamentally different operations "get one of many" vs "get the only one, if correct".  This unfortunate naming is also why Structured Bindings check for tuple_size in addition to checking whether get "works" (ie compiles).  std::get should have been called std::tuple_get: to be more specific, to not steal a general name, and to match tuple_size and tuple_element. And variant should have used something else.

And optional doesn't use get, it uses value().  (But std::value() would also be a bad name for std::get_could_throw())
Smart pointers use get, they are also a warning sign (loss of abstraction) - but a different one from variant (could throw), and different from optional.value() (also could throw).


Anyhow, someone could easily propose a "unchecked_get" for variant.  I think, in this case, it would need to be an ugly name.
Or call it get_ub() since it could lead to Undefined Behaviour, and who doesn't want to get that.


--
Be seeing you,
Tony

Vicente J. Botet Escriba

unread,
Jan 16, 2018, 3:34:52 PM1/16/18
to std-pr...@isocpp.org, Oleksandr Pikozh
Le 16/01/2018 à 07:33, Oleksandr Pikozh a écrit :

In std::variant we have:
- ??? — doesn't check whether a variant contains specified index/type;
- method get — throws std::bad_variant_access when a variant doesn't contain specified index/type.

Why we don't have any fast (unchecked) ways to access std::variant? Assuming context when user has already checked for presence of required item somehow and now needs the fastest access (i.e. he agrees to get UB when absent).

I believe the standard committee could considered it if you have a motivating use case that cannot be managed with std::visit.


std::visit is a safe tool. Adding an unsafe tool when a safe one could be used wouldn't help. We need a use case that will be more efficient with your unsafe function.

Vicente

Vicente J. Botet Escriba

unread,
Jan 16, 2018, 3:37:54 PM1/16/18
to std-pr...@isocpp.org, Zhihao Yuan
Le 16/01/2018 à 16:38, Zhihao Yuan a écrit :
> While it's not a bad idea (pattern matching in other
> languages naturally support this use), in practise
>
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0798r0.html
>
> may be easier to use.
>
It is weird. The committee required to remove from expected<T,E> every
monadic operation. Now we have a paper that request the addition of the
monadic operations to optional. Not that I'm against this member
function approach, but we should be coherent.

Vicente

Tony V E

unread,
Jan 16, 2018, 4:31:48 PM1/16/18
to Standard Proposals
It may be that the committee wanted expected<> to get in first, then add monadic later.  Like optional.
Or it may just be that the committee is not one person, but a bunch, and it changes "its" mind regularly.

Nicol Bolas

unread,
Jan 16, 2018, 5:29:12 PM1/16/18
to ISO C++ Standard - Future Proposals
Or maybe it's just one paper, and a paper being written doesn't mean it's gonna happen.

Nicol Bolas

unread,
Jan 16, 2018, 6:38:00 PM1/16/18
to ISO C++ Standard - Future Proposals, jmck...@gmail.com, o.pi...@gmail.com
On Tuesday, January 16, 2018 at 1:02:56 PM UTC-5, Oleksandr Pikozh wrote:
On 16.01.18 18:35, Nicol Bolas wrote:
On Tuesday, January 16, 2018 at 10:53:38 AM UTC-5, Oleksandr Pikozh wrote:
Well, an object is often used in different ways from different parts of code: more carefree from relatively-rarely-called routines and more optimized from relatively-often-called routines. Therefore it's good for the same tool to support different modes; sometimes I want checking, sometimes I don't (i.e. I want std::variant that keeps its `index`, but sometimes to allow to do the `get`-like thing without internal checks); as for me, that's the main goal of C++ — to support higher-level programming without losing the low-level control. Absence of unchecked operations (comparable to std::optional::operator* and std::vector::operator[]) makes std::variant (almost) useless for me: it looks more psychologically-comfortable to use "enum state + union value" from the beginning, than to integrate std::variant with feeling that it implies checks that I can never get rid of (even in cases when inner type is guaranteed by outer logic).

Besides that the same your argument can be applied for std::optional too — "if you didn't want checking, you'd be using a `T value` instead of `std::optional<T> value`", right? (But no, we still have checkless `std::optional::operator*` — because there is no need to check again in `if (o) {…; f(*o); …;}`-like cases, for example.)

"you're supposed to be visiting it and acting on it that way" — am I supposed to act _only_ in that way?


I generally consider the use of `get` on a variant at all to be a code-smell. A function which takes a variant is a function that is expected to be able to operate correctly on all of the possible values in the variant. And the most effective way to achieve that is to use visitation when you want to access the member. By using `get`, you're saying that the code in question simply can't handle the other possible states of the variant. That's the wrong way to use a variant.

Visitation fixes so many problems. It makes it statically impossible for you to forget to handle a state. It makes your code more readable (to a degree), since you can use the variant value like it were a regular value. It discourages lots of conditions. And so forth.

That's not to say that `get` is bad. It's simply not a tool that should be in such frequent use that not having an unchecked version is a legitimate problem.
Looks like oversimplification for me.
The set of real-life cases is much more rich.
E.g. we can have an std::variant field in a class — and some more-universal methods (that handle all states combinations) in the class can call some more-specialized ones (that handle only specific state combinations).

Yeah, that sounds like broken code just waiting to happen. Such code would be far safer if it were passed a reference to the object it's actually working on, rather than being based on some assumption about it. Yes, I know those are private functions, but you're making it way too easy to accidentally do the wrong thing with your private interface.

Of course, we can cache references (pointers) to members of std::variant and pass them from method to method (in addition to passing `this`, which already has access to the whole std::variant field), but it decreases aesthetics of the code (and additionally looks redundant at first glance, because with the most expected (but not guaranteed, of course) implementation of variant, the pointer to the member would have fixed offset from the pointer to the whole std::variant).

Consider your example of "when inner type is guaranteed by outer logic". How did this "outer logic" provide this guarantee? Did it create the `variant` with that state? If so... why did it bundle it in a `variant`, when it clearly only provides an object of a specific type? Did the "outer logic" already do the test? If so, why did it not unpack the object directly when it did that test, and just pass the object of the right type? If the "inner" logic can only handle a specific state, why is it being given an object that can assume states it cannot handle? That is, rather than:

if(var.index() == required_index)
{
 
auto &value = get<required_index>(var);
}

You do this:

if(auto ptr = get_if<required_index>(&var); ptr)
{
 
//use `ptr`
}

The fundamental difference between `optional` and `variant` is that the former only ever has 2 states. The behavior for the unengaged state will almost always be "do nothing", while the behavior for the engaged state will be "do something". Your code strongly suggests that you're trying to treat a `variant` like an `optional`. And we have a way to do that: get a pointer to the member.
Yes, I consider variant as a generalization of optional.

That's like saying you see an array of integers as a generalization of an integer. I mean yeah, you can see it that way. But you don't use one in the places you'd use the other, and you don't usually use one in any way resembling the way you use the other.

The use cases for these types are so broadly disparate that they don't need to have the same interface.

But (for some reasons that look bogus for me) the generalization isn't smooth. IMHO it is better to fix it (even if no so much people feel practical problems due to that shagginess — just for aesthetic reasons at least).

Also, I'm curious as to your application where a single conditional check is sufficient to make the type "(almost) useless for me". Seriously, is that trivial performance really sufficient to go write your own variant type or to use something less feature-rich?
It's more about consistency and psychology, not about actual benchmarking yet. I am writing right now a piece of code that with high probability will be a part of bottleneck. I realize that in some cases std::variant does redundant checks (at least with the way I use it). I expect them to be avoidable (as in std::optional and others), but they aren't. So it's psychologically simpler for me to throw std::variant out and not to use in the "foundation" (that will probably be a bottleneck) any things that have _known_ overhead.

So it's "psychologically simpler" for you to spend a bunch of time coding something that you might not need, rather than using an off-the-shelf tool that does exactly what you need just possibly slower. Perhaps you're focusing too much on your psychology and not enough on what causes your project to actually get done.

I understand the kind of thing you're talking about. I get that impulse every now and then too, even when I'm coding in non-C++ languages. That micro-optimization voice that says, "ugh, do I really need a conditional branch here? Can't I find a clever way to avoid that?" I even had to stop myself from digging through Lua manuals and such to find a function that could concatenate a bunch of strings without wrapping them in a table.

You would be surprised how much telling that voice to shut up improves your productivity. Without making your code materially slower.

Just consider a variant example:

if(auto var = get_variant(); var.index() == 3)
{
 
auto &value = get<3>(var);
}

Well, the `get` function is visible to the compiler, as is the `index` function. And `3` is a constant expression, so it is visible to the compiler too.

Which means the compiler has 100% of the information available in the local scope to recognize that `get` will do the same check you just did. So if it inlines the `get` call, it is at least theoretically possible for the compiler to optimize out the second test post-inlining.

Does that mean it certainly will? No. But Godbolt says GCC does. It completely optimizes out the test and exception throwing.

Now obviously this is a highly simplified case, and there will be cases where compilers can't reasonably tell. But the farther the initial test is from where it gets used, the more likely it is that you will accidentally call it incorrectly.

Not necessarily variant::get will be a problem (and most probably something absolutely else will), but I don't like a feeling of creating future problems when writing the code (i.e. to write a code with a known and easy-avoidable redundancy, which doesn't brings and significant improvements into the code aesthetics or some other tremendous benefits).

You're also missing another problem with checked vs. unchecked operations: consistency.

In the C++ standard library, the way you tell whether an operation is checked or unchecked is that the unchecked operation uses an operator. `vector::operator[]`, `optional::operator*`, and so on are operators. If you use a named function, then it is a checked operation. `vector::at`, `optional::value`, and so forth.

You can't really have an operator-form of the `variant` `get` function. After all, you need to provide a template index/type, and you can't pass something like that as a function parameter to an operator. After all, operators cannot take explicit template parameters without calling them by spelling out the operator (`variant::operator[]<std::string>(0)`).
I never heard about "methods-are-checked, functions-are-unchecked" rule in stdlib. Is is stated somewhere? 
As for me, much more expected understanding of consistency here would be that every class (where it's applicable) has two ways of access: checked and unchecked. (If the std::vector and std::optional had no unchecked access, then I would have no disturbance about std::variant — I'd just see that as style of C++ stdlib; even if only std::optional had no unchecked access, then it would look understandable for me (stdlib is old, std::vector created long before std::optional and std::variant, style changed since then); but now it looks as breaking its own style.)
And if unchecked things really need to be operators (which I doubt, even if the such rule exists and wasn't broken yet, it will be surely be broken at some point of time in future due to the growing of stdlib and growing of the amount of strongly-needed-to-be-unchecked things), why not simply the explicit cast operator? (Ah, you are right, explicit cast operator will allow only access by type, not by index — still I don't feel both access by index and .)

If you have a legitimate reason to need to have unchecked access to a variant, whatever reasoning you use applies just as well to unchecked access by index. Just because you personally doesn't use it doesn't mean that other people wanting the same thing don't.

Whatever the solution is, it should not make access-by-index a second-class feature of `variant`. Some people really need that to work.

Oleksandr Pikozh

unread,
Jan 17, 2018, 3:05:52 AM1/17/18
to ISO C++ Standard - Future Proposals, Vicente J. Botet Escriba
The `std::visit` simply doesn't allow relying on `index`, for example.

For example

    template<typename T1, typename T2, typename T3>
    void do_something() {
        std::variant<T1, T2, T3> variant = get(…);
        switch (variant.index) {
            case 0: f1(variant.get<0>()); break;
            case 1: f2(variant.get<1>()); break;
            case 2: f3(variant.get<2>()); break;
            default: assert(false);
        }
    }

cannot be replaced with

    template<typename T1, typename T2, typename T3>
    void do_something() {
        std::visit(get(…), [&](auto& arg) {
             using arg_type = std::decay_t<decltype(arg)>;
             if constexpr (std::is_same_v<arg_type, T1>)
                 f1(arg);
             else if constexpr (std::is_same_v<arg_type, T2>)
                 f2(arg);
             else if constexpr (std::is_same_v<arg_type, T3>)
                 f3(arg);
             else
                 assert(false);
         });
    }
   
or even like

    template<typename T1, typename T2, typename T3>
    void do_something() {
        struct visitor {
            void operator()(const T1 &arg) const {f1(arg);}
            void operator()(const T2 &arg) const {f2(arg);}
            void operator()(const T3 &arg) const {f3(arg);}
        };
        std::visit(get(…), visitor());
    }

in cases when we expect <T1, T2, T3> to be possibly duplicate types (e.g. <int, int, std::string>).

Although I don't consider usage with duplicate types and (therefore) relying on index to be important parts of std::variant (personally I, if I designed std::variant, probably wouldn't allow it at all (possibly having it in a different class, like std::multiset vs std::set)), they are allowed, so the way is already chosen. And even I sometimes use it (e.g. when I got a set of having-common-interface-but-non-having-common-ancestor Java-iterator-like classes with `i.hasNext()` and `i.next()`-like methods and was going to build a `concatenate(i1, i2, i3)`-like function). But those who go this way (trying to assume that std::variant allows duplicate types) finally find incompleteness of std::variant for that approach.

P.S.: Sorry, if I described it not enough clearly, I was in hurry. Please, say if it's not understandable — then I may rewrite later.

Vicente J. Botet Escriba

unread,
Jan 17, 2018, 1:01:08 PM1/17/18
to Oleksandr Pikozh, ISO C++ Standard - Future Proposals
Ok, I see the case, but I wonder of the usability of such a variant. The standard doesn't forbid to have twice the same type, but I would never try to use something like that. I would prefer to wrap the types and use std::visit.


Although I don't consider usage with duplicate types and (therefore) relying on index to be important parts of std::variant (personally I, if I designed std::variant, probably wouldn't allow it at all (possibly having it in a different class, like std::multiset vs std::set)), they are allowed, so the way is already chosen. And even I sometimes use it (e.g. when I got a set of having-common-interface-but-non-having-common-ancestor Java-iterator-like classes with `i.hasNext()` and `i.next()`-like methods and was going to build a `concatenate(i1, i2, i3)`-like function). But those who go this way (trying to assume that std::variant allows duplicate types) finally find incompleteness of std::variant for that approach.
I agree that the unsafe access could have a sense in this case and be coherent.
I'll give it a neutral vote as I don't think I need it, but maybe if you can elaborate with a realistic example, I could change my mind.


P.S.: Sorry, if I described it not enough clearly, I was in hurry. Please, say if it's not understandable — then I may rewrite later.
Don't worry, I believe I understood it.


Vicente

Vicente J. Botet Escriba

unread,
Jan 17, 2018, 1:09:45 PM1/17/18
to std-pr...@isocpp.org, Tony V E
Le 16/01/2018 à 22:31, Tony V E a écrit :


On Tue, Jan 16, 2018 at 3:37 PM, Vicente J. Botet Escriba <vicent...@wanadoo.fr> wrote:
Le 16/01/2018 à 16:38, Zhihao Yuan a écrit :
While it's not a bad idea (pattern matching in other
languages naturally support this use), in practise

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0798r0.html

may be easier to use.

It is weird. The committee required to remove from expected<T,E> every monadic operation. Now we have a paper that request the addition of the monadic operations to optional. Not that I'm against this member function approach, but we should be coherent.


It may be that the committee wanted expected<> to get in first, then add monadic later.  Like optional.
Or it may just be that the committee is not one person, but a bunch, and it changes "its" mind regularly.

Then I didn't understood what the committee wanted. I believed that the committee wanted a non-member function interface, hence my Monadic paper.

Vicente

Zhihao Yuan

unread,
Jan 17, 2018, 1:52:40 PM1/17/18
to std-pr...@isocpp.org
I referenced the paper just to show an example
about `and_then` & `or_else`, whether they should members or non-members are not what I planned to comment on.


Sent from ProtonMail mobile



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

Oleksandr Pikozh

unread,
Jan 17, 2018, 2:00:37 PM1/17/18
to ISO C++ Standard - Future Proposals, Vicente J. Botet Escriba
I personally consider such case to be strange for a usual variant, but still perfectly valid and usable for some "special variant" type and some more rare use cases. I.e. I'd create two variant types: something like std::variant (with no type duplication support and probably with no `index` field / `get<index>()` method at all, but with `= value` operator, implicit from-value constructor and implicit to-value cast operator) and std::explicit_variant (with `index` field, with possibility to duplicate types, but without `= value` operator (probably `reset(size_t index, Value value)` method instead), without implicit from-value constructor (maybe `(size_t index, Value value)` constructor instead, with `visit(std::function<void(size_t, Value)>)` visiting), and without implicit to-value cast operator (`get<index>()` method instead)) — with considering the first one as "main", but understanding that some cases need second (like std::set and std::multiset). But stdlib architects decided to merge both in the single class; so that class needs to support full feature set for both approaches.

Oleksandr Pikozh

unread,
Feb 15, 2018, 9:43:07 AM2/15/18
to ISO C++ Standard - Future Proposals, Tony V E
"I think, in this case, it would need to be an ugly name" — sure, that was exactly my thought (I didn't intend a new method to have a nice name).

Does the phrase "anyhow, someone could easily propose a 'unchecked_get' for variant" means that I can try writing a proposal (in the way described in this guide: https://isocpp.org/std/submit-a-proposal)? Or is this practically unpromising?

Thanks.

P.S.: Sorry for answering / continuing the thread only now, at that time I was unable to reply to the rest of the thread.

Nicol Bolas

unread,
Feb 15, 2018, 10:36:07 AM2/15/18
to ISO C++ Standard - Future Proposals, tvan...@gmail.com, o.pi...@gmail.com

You can certainly try it. But:

1. Your proposal will need good motivation. Preferably real-world code, and preferably not a simplified case. The problem with simplified cases is that compilers will often catch them and optimize out the second check. The other thing such motivation requires is show that you genuinely cannot design your code in some other way (by passing a reference/pointer rather than a `variant` or whatever).

Remember: `variant` is supposed to be a safe type; by providing an unchecked-get, you're removing safety from the type. To overcome that, your motivation needs to be unassailable.

2. You'll need someone to go to a standards meeting to get the proposal discussed. That could be yourself or someone else.

Edward Catmur

unread,
Feb 15, 2018, 6:50:07 PM2/15/18
to ISO C++ Standard - Future Proposals, tvan...@gmail.com, o.pi...@gmail.com
I'm not sure whether anyone's pointed this out above, but it is already possible to perform an unchecked get with the help of the optimizer:

*std::get_if<I>(&v)

Since the optimizer will eliminate the index and pointer checks, this is practically equivalent to a hypothetical

std::get_unchecked<I>(v)

So the motivation to add unchecked get would have to be clarity in code, I think.
Reply all
Reply to author
Forward
0 new messages