std::initializer_list{x, y, z} (CTAD) 是否有效?

19

在明确构造一个 std::initializer_list<U> 时,可以使用类模板参数推导(例如使用 CTAD)推断模板参数(U)吗?

换句话说,我知道以下语句是有效的:

std::initializer_list<int> x1{1, 2, 3};
std::initializer_list<int> x2 = {1, 2, 3};
auto x3 = std::initializer_list<int>{1, 2, 3};

但以下陈述是否也有效?

std::initializer_list x1{1, 2, 3};
std::initializer_list x2 = {1, 2, 3};
auto x3 = std::initializer_list{1, 2, 3};

编译器对于 std::initializer_list 的模板参数能否被推导存在分歧:

#include <initializer_list>

struct s {
    s(std::initializer_list<int>);
};

void f() {
    std::initializer_list x1{1, 2, 3};         // Clang ERROR; GCC OK;    MSVC OK
    std::initializer_list x2 = {1, 2, 3};      // Clang ERROR; GCC OK;    MSVC OK
    auto x3 = std::initializer_list{1, 2, 3};  // Clang ERROR; GCC OK;    MSVC OK

    s x4(std::initializer_list{1, 2, 3});      // Clang ERROR; GCC ERROR; MSVC OK
    s x5{std::initializer_list{1, 2, 3}};      // Clang ERROR; GCC OK;    MSVC OK
    s x6 = s(std::initializer_list{1, 2, 3});  // Clang ERROR; GCC OK;    MSVC OK
    s x7 = s{std::initializer_list{1, 2, 3}};  // Clang ERROR; GCC OK;    MSVC OK
    s x8 = std::initializer_list{1, 2, 3};     // Clang ERROR; GCC OK;    MSVC OK

    void g(std::initializer_list<int>);
    g(std::initializer_list{1, 2, 3});         // Clang ERROR; GCC OK;    MSVC OK
}

(在Compiler Explorer上查看此示例。)

已测试的编译器:

  • 使用-std=c++17 -stdlib=libc++-std=c++17 -stdlib=libstdc++的Clang版本7.0.0
  • 使用-std=c++17的GCC版本8.3
  • 使用/std:c++17的MSVC版本19.16
2个回答

10

Clang是唯一正确的编译器。是的,真的。

当编译器看到没有模板参数的模板名称时,它必须查看模板的推导向导并将其应用于括号初始化列表中的参数。initializer_list没有任何显式的推导向导,因此它使用可用的构造函数。

initializer_list具有的唯一公开可访问的构造函数是其复制/移动构造函数和默认构造函数。从大括号初始化列表创建std::initializer_list不是通过公开可访问的构造函数完成的,而是通过列表初始化完成的,这是一个仅限编译器的过程。只有编译器才能执行构建序列所需的步骤

综合考虑,除非从现有列表中进行复制,否则不应该在initializer_list上使用CTAD。最后一个部分可能是其他编译器在某些情况下使其正常工作的原因。就推断而言,它们可以将花括号初始化列表作为initializer_list <T>本身来推导,而不是将其视为要应用[over.match.list]的参数序列,因此推断指南看到的是一个复制操作。


1
你认为这应该被允许吗?因为在我看来,这似乎是一个疏忽。 - Rakete1111
4
我不太确定。您使用了[over.match.class.deduct],其中包括从复制推断候选项中形成函数模板。该函数模板将类似于template<class T> auto X(std::initializer_list<T>) -> std::initializer_list<T>;然后[over.match.class.deduct]指示使用提供的初始化器{1,2,3},将函数模板视为初始化某些虚构类类型的构造函数模板,然后进入[over.match.list]。如果这样做,您将推断出T=int - T.C.
@T.C.,你说的很有道理。如果我添加以下扣除指南(应与复制扣除候选人相同),那么Clang现在会编译我的测试程序中的每一行(就像MSVC一样):namespace std { template<class T> initializer_list(std::initializer_list<T>) -> initializer_list<T>; } - strager
@T.C.:“*那个函数模板看起来像是template<class T> auto X(std::initializer_list<T>) -> std::initializer_list<T>;*” 如果它应该是这样工作的,那就可以解释为什么Clang会出错了。那是一个初始化列表构造函数,在[over.match.list]中具有优先级。但如果Clang的推导实现不将其视为初始化列表构造函数,或者如果其推导实现不适用于[over.match.list]的规则,则可能会忽略该指南。它可能会寻找一个三元素构造函数。 - Nicol Bolas

3
这是一个Clang的bug,正如Nicol Bolas从未解决的答案评论中所得出的结论。总之,像任何类类型一样,std::initializer_list具有编译器提供的模板类型推导指南[over.match.class.deduct]§1.3

从假设的构造函数C(C)派生出的另一个函数模板,称为复制推导候选函数。

这意味着std::initializer_list隐式地声明了该推导指南:

template <class T>
initializer_list (initializer_list <T>) -> initializer_list <T>;

当使用非空的初始值列表参数来推导此推导指南的T时,适用标准的以下规则[temp.deduct.call]§1

如果从P中去掉引用和cv限定符产生std::initializer_list<P′>或者P′[N],其中P′和N是一些类型和数值,并且该参数是一个非空初始值列表[dcl.init.list],那么将独立地为初始化列表的每个元素执行推断,将P′作为单独的函数模板参数类型P′i,将第i个初始化元素作为相应的参数。

因此,T应该被推导为int,因此类模板参数演绎将成功。 免责声明:我不是Clang的拥护者...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接