当此构造函数采用初始化列表并委托向量时,是什么导致此构造函数委托给自身?

What causes this constructor to delegate to itself when it takes an initializer list and delegates a vector?

#include <initializer_list>
#include <vector>

struct test
{
    using t = std::vector<test>;
    test(t const &v)
    {
    }
    test(t &&v)
    {
    }
    test(std::initializer_list<test> v)
    : test{t{v}} //error
    {
    }
};

Clang and GCC 都抱怨第三个构造函数,即采用初始化列表的构造函数委托给自己。我不明白这是怎么可能的,因为你不能从向量构造一个初始化列表。

通过用圆括号替换外部花括号来修复错误是微不足道的,但为什么这首先会成为一个问题?这个几乎相同的程序编译得很好:

#include <initializer_list>

struct a {};
struct b {};

struct test
{
    test(a const &)
    {
    }
    test(a &&)
    {
    }
    test(std::initializer_list<b> v)
    : test{a{}} //no error, still using curly braces
    {
    }
};

有趣的是,对于上面的第二个示例,如果将 b 替换为 test,错误会再次出现。有人可以解释这里发生了什么吗?

您的代码中的问题是您正在编写一个调用自身的函数。

test(std::initializer_list<test> v)
    : test{t{v}} //error
    {
    }

test{t{v}} 将首先以 v 作为参数调用 vector<test> 的初始化列表。但是 vector 将再次调用该函数来初始化将失败的值。

编译器不知道如何解决这个问题。将初始化更改为使用括号将(如您所说)解决此问题,因为它将在 vector 中调用隐式复制构造函数(什么都不做,因为您的结构没有做任何事情)。

第二个示例以此调用开始:a{}

它很好地解决了,因为 a 是一个具有隐式定义构造函数的基本结构。第二个电话是 test{...} 一个

因为类型是 a 并且有 a 的构造函数所以它运行得很好。

t{v}的类型是std::vector<test>。这个想法是 init-list 构造函数总是优先于任何其他构造函数,因此 test{t{v}} 将首先尝试调用 init-list 构造函数,如果存在,并且类型兼容。在你的情况下,这是可能的,因为 test 本身可以从 std::vector<test> 隐式构造(通过你的前 2 个构造函数),所以编译器最终委托给 init-list 构造函数,因此错误.

在第二种情况下,没有歧义,因为类型 a{} 不能再隐式转换为 std::initializer_list<b>.

在第一个示例中构造构造函数 explicit,或者用 test(t{v}) 调用基构造函数,您的歧义将消失(编译器将不再执行隐式转换)。

一个更简单的示例(实时 here)表现出与您的第一个示例基本相同的行为是:

#include <initializer_list>

struct test
{
    /*explicit*/ test(int){} // uncomment explicit and no more errors
    test( std::initializer_list<test> v)
        : test{42} {} // error, implicitly converts 42 to test(42) via the test(int)
};

int main(){}

处理 init-list 构造函数的标准的相关部分是 §13.3.1.7/1 [over.match.list] - 下面的引文摘自@Praetorian 现在已删除的答案 -

When objects of non-aggregate class type T are list-initialized such that 8.5.4 specifies that overload resolution is performed according to the rules in this section, overload resolution selects the constructor in two phases:
Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.
— If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

我不是 C++ 专家,但主要区别在于

test{a{}}

initializer_list<a> 没有重载,因此构造函数不可用。

另一方面,test{t{v}} 确实有一个可用的 initializer_list<test> 构造函数,因为您可以从 vector 创建一个 test。它可以使用(我不知道规则的名称)1 cast 转换。

test{t{v}} -> test{test(t{v})}