std::variant 转换构造函数不处理 const volatile 限定符

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

代码如下:

               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;

输出:

true
false
false
false
0
0
0
0

coliru

因此 std::variant 转换构造函数不考虑转换自类型的 const volatile 限定符。这是预期的行为吗?

关于从 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...

问题是在上面的例子中,这种虚函数的重载集是不明确的:

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

coliru

cppreference.com 对这个案例只字不提。标准对此有规定吗?

我正在自己实现 std::variant class。我转换构造函数的实现是基于。结果与上图相同(选择了第一个合适的备选方案,即使还有其他备选方案)。 libstdc++ 可能以相同的方式实现它,因为它也会选择第一个合适的替代方案。但我仍然想知道这是否是正确的行为。

是的,这就是按值传递时函数的工作方式。

函数void foo(int)和函数void foo(const int)和函数void foo(volatile int)和函数void foo(const volatile int)are all the same function.

通过扩展,您的变体的转换构造函数没有区别,并且没有有意义的方式来使用其替代品仅在其顶级 cv-qualifier 上不同的变体.

(好吧,您可以 emplace 使用显式模板参数,如 Marek 所示,但为什么呢?目的是什么?)

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

请注意,您正在创建值的副本。这意味着可以安全地丢弃 constvolatile 修饰符。这就是为什么模板总是推导 int.

您可以使用 emplace 强制指定类型。

观看演示 ​​https://coliru.stacked-crooked.com/a/4dd054dc4fa9bb9a

我对标准的理解是,由于歧义,代码应该是格式错误的。令我惊讶的是 libstdc++ 和 libc++ 似乎都允许它。

[variant.ctor]/12 是这样说的:

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.

所以创建了四个函数:最初是FUN(int), FUN(const int), FUN(volatile int),以及 FUN(const volatile int)。这些都是等效的签名,因此它们不能相互重载。本段并没有真正指定如果无法实际构建重载集应该发生什么。但是,有一个注释强烈暗示了特定的解释:

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

这个注释基本上是说重载决议不能区分stringstring。为了实现这一点,即使签名相同,也必须进行重载解析。两个 FUN(string) 没有折叠成一个函数。

请注意,由于模板原因,允许重载决策考虑具有相同签名的重载。例如:

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

这里有两个相同的f签名,都提交给重载决议,但没有一个比另一个更好。

回到标准的例子,似乎处方是应用重载决议程序,即使一些重载不能像普通函数声明一样相互重载。 (如果你愿意,可以想象它们都是从模板实例化的。)然后,如果重载决议不明确,std::variant 转换构造函数调用是错误的。

注释并没有说 variant<string, string> 示例格式错误,因为重载决策选择的类型在替代列表中出现了两次。它说重载决议本身是不明确的(因为这两种类型具有同样可行的构造函数)。这种区别很重要。如果此示例在重载决议阶段之后被拒绝,则可以提出您的代码格式正确的论点,因为顶级 cv 限定符将从参数类型中删除,使得所有四个重载 FUN(int) 因此 T_j = int。但是由于该注释表明 重载解析期间失败,这意味着您的示例不明确(因为 4 个签名是等效的)并且必须对此进行诊断。