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

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


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


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.