使用列表初始化的不明确构造函数调用


struct A {
A(int) {}
};
struct B {
B(A) {}
};
int main() {
B b({0});
}

b的构造给出以下错误:

In function 'int main()':
24:9: error: call of overloaded 'B(<brace-enclosed initializer list>)' is ambiguous
24:9: note: candidates are:
11:2: note: B::B(A)
10:8: note: constexpr B::B(const B&)
10:8: note: constexpr B::B(B&&)

我期待B::B(A)被召唤,为什么在这种情况下模棱两可?

给定一个类,A使用用户定义的构造函数:

struct A
{
A(int) {}
};

另一个,B,接受A作为构造函数参数:

struct B
{
B(A) {}
};

然后为了执行如下初始化:

B b({0});

编译器必须考虑以下候选项:

B(A);         // #1
B(const B&);  // #2
B(B&&);       // #3

尝试查找从{0}到每个参数的隐式转换序列。

请注意,B b({0})不会b列表初始化 -- (copy-)list-初始化适用于构造函数参数本身。

由于参数是初始值设定项列表,因此将参数与参数匹配所需的隐式转换序列根据列表初始化序列 [over.ics.list]/p1 定义:

当参数是初始值设定项列表 ([dcl.init.list]) 时,它不是表达式,特殊规则适用于将其转换为参数类型。

上面写着:

[...],如果参数是非聚合类 X,并且每个 13.3.1.7 的重载分辨率选择单个 X 的最佳构造函数,用于从参数初始值设定项列表中执行 X 类型的对象的初始化, 隐式转换序列是用户定义的转换序列,具有第二个标准转换 对标识转换进行排序。如果多个构造函数可行,但没有一个比其他构造函数更好,则 隐式转换序列是模糊的转换序列。允许用户定义的转换 用于将初始值设定项列表元素转换为构造函数参数类型,但 13.3.3.1 中所述除外。

要使 #1 可行,以下调用必须有效:

A a = {0};

由于 [over.match.list]/p1,这是正确的:

— 如果未找到可行的初始值设定项列表构造函数,

则再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始值设定项列表的元素组成。

即,类A有一个接受int参数的构造函数。

要使 #2 成为有效的候选项,以下调用必须有效:

const B& b = {0};

根据 [over.ics.ref]/p2:

当引用类型的参数未直接绑定到参数表达式时,转换序列是根据 [over.best.ics 将参数表达式转换为引用类型所需的转换序列。从概念上讲,此转换序列对应于使用参数表达式复制初始化引用类型的临时。顶级简历资格的任何差异都包含在初始化本身中,不构成转换。

翻译为:

B b = {0};

再一次,在 [over.ics.list]/p6 之后:

允许用户定义的转换,以将初始值设定项列表元素转换为构造函数参数类型 [...]

允许编译器使用用户定义的转换:

A(int);

将参数0转换为B的构造函数参数A

对于候选人#3,相同的推理适用于#2。最终,编译器无法在上述隐式转换序列之间进行选择{需要引用},并报告歧义。

代码在GCC8中编译良好。

这不应该是模棱两可的召唤。对于正在调用的B的复制/移动构造函数,对于B b({0});,需要执行以下步骤:

  1. 通过A::A(int)0构建A
  2. 从步骤 1 中构造的A构造BB::B(A)
  3. 从步骤2中构造B构造bB的复制/移动构造函数。

这意味着需要两个用户定义的转换(step#1 和 #2),但不允许在一个隐式转换序列中这样做。

B b({0})可能会导致调用以下任一内容:

  1. B::B(A)

  2. 复制B构造函数:从{0}构造一个临时B对象 然后将其复制到b.

因此模棱两可。

如果调用B b{0},则可以解决此问题,它直接使用定义的构造函数而不涉及复制构造函数。

编辑:

关于第2点的有效性:

B有一个接受A的构造函数。现在,A可以通过int来构建。此外,可以通过初始化列表构造int。这就是为什么这是一个有效的案例。如果A的构造函数是explicit的,从{0}int的自动转换就会失败,导致没有歧义。

最新更新