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

Is it possible to invoke a user-defined conversion function via list-initialization?

本文关键字:定义 转换 函数 用户 调用 可以通过 列表 初始化 是否      更新时间:2023-10-16

这个程序合法吗?

struct X { X(const X &); };
struct Y { operator X() const; };
int main() {
X{Y{}};   // ?? error
}

在 n2672 之后,经缺陷 978 修订,13.3.3.1 [over.best.ics]具有:

4 - 但是,当考虑作为候选 [...] by 13.3.1.7 的构造函数或用户定义的转换函数的参数时 [...] 当初始值设定项列表只有一个元素并且对于 X 的构造函数的第一个参数考虑转换为某个类 X 或引用(可能符合 cv 条件的)X 时 [...],仅考虑标准转换序列和省略号转换序列。

这似乎相当反常;它的结果是使用列表初始化强制转换指定转换是非法的:

void f(X);
f(Y{});     // OK
f(X{Y{}});  // ?? error

据我了解 n2640,列表初始化应该能够取代直接初始化和复制初始化的所有使用,但似乎没有办法仅使用 list-initialization从Y类型的对象构造X类型的对象:

X x1(Y{});  // OK
X x2 = Y{}; // OK
X x3{Y{}};  // ?? error

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

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 将候选函数限制为生成类型T的类S上的函数,其中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 条件)X 时,考虑将转换为某个类 X 或引用(可能符合 cv 条件的)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

那么问题就是提及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});
}

有两个可用于列表初始化的构造函数;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}});

编辑:在经历了所有这些之后,我发现了Johannes Schaub的讨论,证实了这一分析:

这旨在分解列表初始化的复制构造函数 因为由于我们被允许使用嵌套的用户定义转换,我们 总是可以通过第一次调用产生模棱两可的第二个转换路径 复制构造函数,然后执行与我们对另一个相同的操作 转换。


好的,开始提交缺陷报告。 我建议拆分 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],

仅考虑标准转换序列和省略号转换序列;当考虑由 13.3.1.7 [over.match.list] 作为候选X的类的构造函数的第一个参数时,或者当初始值设定项列表只有一个元素传递时,用户定义的转换X或引用(可能是CV)-qualified) 仅当转换函数指定其用户定义的转换时,才考虑X。[注意:因为在列表初始化的上下文中隐式转换序列中允许多个用户定义的转换,因此此限制是必要的,以确保使用非类型X或派生自X类型的单个参数a调用的X的转换构造函数不会对使用从a构造的临时X对象本身调用的X构造函数产生歧义。-- 尾注]

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 的。

有趣的是,该标准如何谨慎地将限制仅应用于副本初始化的第二步,该步骤从已经具有正确类型的临时副本(并且可能通过用户定义的转换获得!然而,对于列表初始化,似乎不存在类似的机制......