是否可以通过列表初始化来调用用户定义的转换函数?

eca*_*mur 6 c++ implicit-conversion c++11 list-initialization

这个程序合法吗?

struct X { X(const X &); };
struct Y { operator X() const; };

int main() {
  X{Y{}};   // ?? error
}
Run Code Online (Sandbox Code Playgroud)

n2672之后,经过缺陷978的修正,13.3.3.1 [over.best.ics]具有:

4 - 但是,当初始化列表只有一个元素并且转换为某个类X时,在考虑构造函数或用户定义的转换函数的参数时,它是13.3.1.7 [...]的候选者.或者引用(可能是cv-qualified)X被认为是X [...]的构造函数的第一个参数,只考虑标准转换序列和省略号转换序列.

这看起来很不正常; 它的结果是使用列表初始化强制转换指定转换是非法的:

void f(X);
f(Y{});     // OK
f(X{Y{}});  // ?? error
Run Code Online (Sandbox Code Playgroud)

据我所知n2640,列表初始化应该能够替换直接初始化和复制初始化的所有用法,但是似乎没有办法只使用list-initialization X从类型的对象构造一个类型的对象Y:

X x1(Y{});  // OK
X x2 = Y{}; // OK
X x3{Y{}};  // ?? error
Run Code Online (Sandbox Code Playgroud)

这是标准的实际意图吗?如果没有,它应该如何阅读或阅读?

eca*_*mur 6

13.3.3.1p4的初衷是描述如何在12.3p4中应用以下要求:

4 - 最多一个用户定义的转换(构造函数或转换函数)隐式应用于单个值.

缺陷84之前,13.3.3.1p4 几乎是纯粹的信息:

4 - 在用户定义的转换初始化的上下文中(即,在考虑用户定义的转换函数的参数时;参见13.3.1.4 [over.match.copy],13.3.1.5 [over.match.conv] ),只允许标准转换序列和省略号转换序列.

这是因为13.3.1.4第1段子弹2和13.3.1.5p1b1将候选函数限制为类S生成类型T,其中S是初始化表达式的类类型,是初始化T对象的类型,因此没有纬度用于插入另一个用户定义的转换转换序列.(13.3.1.4p1b1是另一个问题;见下文).

缺陷84修复auto_ptr漏洞(即auto_ptr<Derived> -> auto_ptr<Base> -> auto_ptr_ref<Base> -> auto_ptr<Base>通过限制转换序列允许在类复制初始化的第二步骤(这里的构造的构造的单个参数,通过两个转换函数以及转换构造函数)auto_ptr<Base>服用auto_ptr_ref<Base>,禁止使用的转换函数以转换其参数auto_ptr<Base>:

4 - 但是,当在类复制初始化的第二步中调用临时复制时,考虑13.3.1.3 [over.match.ctor]的候选用户定义转换函数的参数时,或者在所有情况下,通过13.3.1.4 [over.match.copy],13.3.1.5 [over.match.conv]或13.3.1.6 [over.match.ref],仅允许标准转换序列和省略号转换序列.

n2672然后补充:

[...]通过13.3.1.7 [over.match.list]将初始化列表作为单个参数传递,或者初始化列表只有一个元素并转换为某个类X或引用(可能是cv-qualified) X被认为是X的构造函数的第一个参数,[...]

这显然是混淆的,因为13.3.1.3和13.3.1.7中唯一的候选转换是构造函数,而不是转换函数. 缺陷978纠正了这个:

4 - 但是,在考虑构造函数或用户定义的转换函数的参数时[...]

这也使13.3.1.4p1b1与12.3p4一致,否则将允许在复制初始化中无限制地应用转换构造函数:

struct S { S(int); };
struct T { T(S); };
void f(T);
f(0);   // copy-construct T by (convert int to S); error by 12.3p4
Run Code Online (Sandbox Code Playgroud)

问题是13.3.1.7所指的语言是什么意思. X正在复制或移动构造,因此语言不包括应用用户定义的转换来获得其X参数. std::initializer_list没有转换函数,所以语言必须适用于其他东西; 如果不打算排除转换函数,则必须排除转换构造函数:

struct R {};
struct S { S(R); };
struct T { T(const T &); T(S); };
void f(T);
void g(R r) {
    f({r});
}
Run Code Online (Sandbox Code Playgroud)

列表初始化有两个可用的构造函数; T::T(const T &)T::T(S).通过排除复制构造函数(因为它的参数需要通过用户定义的转换序列进行转换),我们确保只T::T(S)考虑正确的构造函数.如果没有这种语言,列表初始化将是模糊的.将初始化列表作为单个参数传递的方式类似:

struct U { U(std::initializer_list<int>); };
struct V { V(const V &); V(U); };
void h(V);
h({{1, 2, 3}});
Run Code Online (Sandbox Code Playgroud)

编辑:并通过所有那一切,我发现一个讨论约翰内斯·绍布说证实了这一分析:

这是为了将列表初始化的复制构造函数分解出来,因为我们被允许使用嵌套的用户定义转换,所以我们总是可以通过首先调用复制构造函数然后像对待其他构造函数那样生成一个模糊的第二个转换路径.转换.


好的,关闭提交缺陷报告.我打算建议拆分13.3.3.1p4:

4 - 但是,在考虑构造函数或用户定义的转换函数的参数时:

  • 通过13.3.1.3 [over.match.ctor]在类复制初始化的第二步中复制临时时调用,或者
  • 在所有情况下,通过13.3.1.4 [over.match.copy],13.3.1.5 [over.match.conv]或13.3.1.6 [over.match.ref],

只考虑标准转换序列和省略号转换序列; 当X将初始化列表作为单个参数传递或初始化列表只有一个元素时,在考虑13.3.1.7 [over.match.list]的候选类的构造函数的第一个参数时,用户定义的转换到X或参考(可能CV -qualified)X如果由转换函数中指定其用户定义的转换只考虑.[ 注意:因为在列表初始化的上下文中隐式转换序列中允许多个用户定义的转换,所以这个限制是必要的,以确保转换构造函数X,使用a不属于类型的单个参数调用X或类型派生自X,对于使用构造的X临时X对象调用的构造函数不是不明确的a.- 结束说明 ]


fgp*_*fgp 3

XCode 4.4 附带的 clang 3.1 版本同意您的解释并拒绝X{Y{}};。正如我一样,在重新阅读了几次标准的相关部分后,FWIW。

如果我修改 的X构造函数以采用两个类型的参数const X&,则 clang 接受该语句Y y; X{y,y}。(如果我尝试它就会崩溃X{Y{},Y{}}......)。这似乎与 13.3.3.1p4 一致,后者要求仅在单元素情况下跳过用户定义的转换。

似乎最初仅在已经发生另一个用户定义的转换的情况下才添加对标准和省略号转换序列的限制。或者至少我是这么读的http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#84

有趣的是,该标准小心翼翼地将限制应用于复制初始化的第二步,该步骤从已经具有正确类型的临时对象进行复制(并且可能通过用户定义的转换获得!)。然而对于列表初始化,似乎不存在类似的机制......

  • 听起来像是规范中的缺陷。最好这样报告。当“X{Y{}}”不起作用时,“X{Y{}, Y{}}”没有理由起作用。 (2认同)