Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::variant converting constructor doesn't handle const volatile qualifiers

The code below:

               int i    = 1;
const          int i_c  = 2;
      volatile int i_v  = 3;
const volatile int i_cv = 4;

typedef std::variant<int, const int, volatile int, const volatile int> TVariant;

TVariant var   (i   );
TVariant var_c (i_c );
TVariant var_v (i_v );
TVariant var_cv(i_cv);

std::cerr << std::boolalpha;

std::cerr << std::holds_alternative<               int>(var   ) << std::endl;
std::cerr << std::holds_alternative<const          int>(var_c ) << std::endl;
std::cerr << std::holds_alternative<      volatile int>(var_v ) << std::endl;
std::cerr << std::holds_alternative<const volatile int>(var_cv) << std::endl;

std::cerr << var   .index() << std::endl;
std::cerr << var_c .index() << std::endl;
std::cerr << var_v .index() << std::endl;
std::cerr << var_cv.index() << std::endl;

outputs:

true
false
false
false
0
0
0
0

coliru

And so std::variant converting constructor doesn't take into account const volatile qualifier of the converting-from type. Is it expected behavior?

Information about converting constructor from cppreference.com

Constructs a variant holding the alternative type T_j that would be selected by overload resolution for the expression F(std::forward<T>(t)) if there was an overload of imaginary function F(T_i) for every T_i from Types...

The problem is that in the case above the overload set of such imaginary function is ambiguous:

void F(               int) {}
void F(const          int) {}
void F(      volatile int) {}
void F(const volatile int) {}

coliru

cppreference.com says nothing about this case. Does the standard specify this?

I'm making my own implementation of std::variant class. My implementation of converting constructor is based on this idea. And the result is the same as shown above (the first suitable alternative is selected, even though there are others). libstdc++ probably implements it in the same way, because it also selects the first suitable alternative. But I'm still wondering if this is correct behavior.

like image 897
anton_rh Avatar asked Sep 13 '19 13:09

anton_rh


People also ask

Does std :: variant allocate memory?

One additional thing is that std::variant does not dynamically allocate memory for the values. Any instance of std::variant at any given time either holds a value of one of its alternative types, or it holds no value at all.

Does STD variant use RTTI?

Since this function is specific to a given type, you don't need RTTI to perform the operations required by std::any .

How does C++ variant work?

It holds one of several alternatives in a type-safe way. No extra memory allocation is needed. The variant needs the size of the max of the sizes of the alternatives, plus some little extra space for knowing the currently active value. By default, it initializes with the default value of the first alternative.

Can std :: variant be empty?

Empty variants are also ill-formed (std::variant<std::monostate> can be used instead). A variant is permitted to hold the same type more than once, and to hold differently cv-qualified versions of the same type.


3 Answers

Yeah, this is just how functions work when you pass by value.

The function void foo(int) and the function void foo(const int) and the function void foo(volatile int) and the function void foo(const volatile int) are all the same function.

By extension, there is no distinction for your variant's converting constructor to make, and no meaningful way to use a variant whose alternatives differ only in their top-level cv-qualifier.

(Well, okay, you can emplace with an explicit template argument, as Marek shows, but why? To what end?)

[dcl.fct/5] [..] After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type. [..]

like image 70
Lightness Races in Orbit Avatar answered Oct 23 '22 12:10

Lightness Races in Orbit


Note that you are creating copy of value. This means that const and volatile modifiers can be safely discarded. That is why template always deduces int.

You can force specific type using emplace.

See demo https://coliru.stacked-crooked.com/a/4dd054dc4fa9bb9a

like image 44
Marek R Avatar answered Oct 23 '22 11:10

Marek R


My reading of the standard is that the code should be ill-formed due to ambiguity. It surprises me that both libstdc++ and libc++ appear to allow it.

Here's what [variant.ctor]/12 says:

Let T_j be a type that is determined as follows: build an imaginary function FUN(T_i) for each alternative type T_i. The overload FUN(T_j) selected by overload resolution for the expression FUN(std::forward<T>(t)) defines the alternative T_j which is the type of the contained value after construction.

So four functions are created: initially FUN(int), FUN(const int), FUN(volatile int), and FUN(const volatile int). These are all equivalent signatures, so they could not be overloaded with each other. This paragraph does not really specify what should happen if the overload set cannot actually be built. However, there is a note that strongly implies a particular interpretation:

[ Note:
  variant<string, string> v("abc");
is ill-formed, as both alternative types have an equally viable constructor for the argument. —end note]

This note is basically saying that overload resolution cannot distinguish between string and string. In order for that to happen, overload resolution must be done even though the signatures are the same. The two FUN(string)s are not collapsed into a single function.

Note that overload resolution is allowed to consider overloads with identical signatures due to templates. For example:

template <class T> struct Id1 { using type = T; };
template <class T> struct Id2 { using type = T; };
template <class T> void f(typename Id1<T>::type x);
template <class T> void f(typename Id2<T>::type x);
// ...
f<int>(0);  // ambiguous

Here, there are two identical signatures of f, and both are submitted to overload resolution but neither is better than the other.

Going back to the Standard's example, it seems that the prescription is to apply the overload resolution procedure even if some of the overloads could not be overloaded with each other as ordinary function declarations. (If you want, imagine that they are all instantiated from templates.) Then, if that overload resolution is ambiguous, the std::variant converting constructor call is ill-formed.

The note does not say that the variant<string, string> example was ill-formed because the type selected by overload resolution occurs twice in the list of alternatives. It says that the overload resolution itself was ambiguous (because the two types had equally viable constructors). This distinction is important. If this example were rejected after the overload resolution stage, an argument could be made that your code is well-formed since the top-level cv-qualifiers would be deleted from the parameter types, making all four overloads FUN(int) so that T_j = int. But since the note suggests a failure during overload resolution, that means your example is ambiguous (as the 4 signatures are equivalent) and this must be diagnosed.

like image 23
Brian Bi Avatar answered Oct 23 '22 11:10

Brian Bi