隐式对的模板参数推导

Template argument deduction for implicit pair

考虑以下代码:

#include <utility>
#include <initializer_list>

template <typename T>
struct S {
    S(std::initializer_list<std::pair<T, int>>) {};
};

int main()
{
    S s1 {{42, 42}};          // failed due to implicit `std::pair` from `{42, 42}`
    S s2 {std::pair{42, 42}}; // ok
}

s1 由于从大括号初始化器列表创建隐式 std::pair,实例无法编译。

有没有办法(可能通过用户定义的类型推导指南)使 s1 可编译而无需在声明中指定其他类型?

遗憾的是没有。

原因是模板参数不能通过嵌套大括号 (std::initializer_list) 推导出来,因为它们出现在 non-deduced context 中。有关此行为的更多示例,请参阅 link。

为了不将 CTAD 拖入其中,您的示例相当于:

#include <utility>
#include <initializer_list>

template <typename T>
void S(std::initializer_list<std::pair<T, int>>) {};


int main()
{
    S({{42, 42}});          
    S ({std::pair{42, 42}}); 
}

现在让我们看看这些示例失败的确切原因。该标准对论点推导如下:(强调我的)

Template argument deduction is done by comparing each function template parameter type (call it P) that contains template-parameters that participate in template argument deduction with the type of the corresponding argument of the call (call it A) as described below. If removing references and cv-qualifiers from P gives std::initializer_list<P'> or P'[N] for some P' and N and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list, taking P' as a function template parameter type and the initializer element as its argument, and in the P'[N] case, if N is a non-type template parameter, N is deduced from the length of the initializer list. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context[temp.deduct.call]

因此,因为 S 接受初始化列表,编译器首先尝试从列表中的每个参数推断其内部类型,并且它们必须匹配。这意味着这个辅助函数是为了推导而创建的:

template <typename X> void foo(X element);

并使用内部列表元素调用,在我们的例子中是 {42,42},导致 foo({42,42})。问题就在这里,你不能从中推断出 X,关键是甚至没有关于 std::pair 的任何信息,所以这个任务根本不可能,并且被标准明确禁止为非推断上下文:

The non-deduced contexts are:

...

5.6 A function parameter for which the associated argument is an initializer list ([dcl.init.list]) but the parameter does not have a type for which deduction from an initializer list is specified ([temp.deduct.call]). [ Example:

template void g(T); g({1,2,3}); // error: no argument deduced for T

— end example ] [temp.deduct.type]

现在应该清楚为什么 S({std::pair{42, 42}}) 会起作用了。因为外部列表的参数以 std::pair<int,int> 的形式给出(在调用之前由 CATD 推导),所以传递给 foo 的类型只是 X=std::pair<int,int>。列表 std::initializer_list<std::pair<int,int>> 现在可以与函数声明匹配以推导 T=int,从而导致调用成功。 如果至少有一个成功,则所有内部元素都尝试独立地推导 X,那些没有成功的元素必须至少可以隐式转换。如果更多的人成功了,他们一定推导出了完全相同的类型。

S({{42, 42}, std::pair{1,2},{2,3}}); // Works
// Error, two deduced types do not match.
S({{42, 42}, std::pair{1,2},std::pair{(char)2,3}}); 
// Fine, std::pair<int,int> is deduced, others fail but can be converted to it.
S({{42, 42}, std::pair{1,2},{(char)2,3}, {(float)2,3}}); 

另一种方法是简单地手动指定T,然后不需要任何推导,只需匹配参数:

S<int>({{42, 42}, {1,2},{2,3}});

我不太清楚为什么规则是这样的,也许存在更多的模板参数有一些问题。就个人而言,现在看这个,我觉得可以有更详细的 foo 继承内部列表签名,如:

template<typename T>
void foo(std::pair<T,int>);

然后将 T 传递回去。

您的尝试失败的原因与此方法有效的原因完全相同:

#include <array>
#include <iostream>
#include <utility>
#include <initializer_list>

struct S1 {
    S1(std::initializer_list<std::array<int,2>>) {
        std::cout << "S1\n";
    };
};

struct S2 {
    S2(std::initializer_list<std::pair<int, int>>) {
        std::cout << "S2\n";
    };
};

int main()
{
    S1 s1 {{42, 42}}; // Prints S1
    S2 s2 {{42, 42}}; // Prints S2
}