Search code examples
c++variantstd

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.


Solution

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