嵌套的 initializer_list 何时不明确,为什么模板会影响其行为?

问题描述

我遇到了一个有趣的行为,其中模板似乎会影响嵌套的 std::initializer_list 是否不明确。考虑以下示例:

#include <initializer_list>
#include <iostream>

template <typename T = int>
void constructor_T(std::initializer_list<T> l) {
    std::cout << "constructor_T 1D" << std::endl;
}

template <typename T = int>
void constructor_T(std::initializer_list<std::initializer_list<T>> ll) {
    std::cout << "constructor_T 2D" << std::endl;
}

void constructor_int(std::initializer_list<int> l) {
    std::cout << "constructor_int 1D" << std::endl;
}

void constructor_int(std::initializer_list<std::initializer_list<int>> ll) {
    std::cout << "constructor_int 2D" << std::endl;
}

int main() {
    constructor_T({});               // constructor_T 2D,why not ambiguous?
    constructor_T({{},{}});         // constructor_T 2D,why not ambiguous?
    constructor_T({1,2,3,4});     // constructor_T 1D
    constructor_T({{1,2},{3,4}}); // constructor_T 2D

    constructor_int({});                // ambiguous
    constructor_int({{},{}});          // ambiguous
    constructor_int({1,4});      // constructor_int 1D
    constructor_int({{1,4}});  // constructor_int 2D

    return 0;
}

constructor_int 几乎与 constructor_T 相同,只是 constructor_int 不是模板化的。当调用 constructor_int 且初始化器列表为空时,编译器会抱怨歧义,但是 constructor_T 工作正常。

错误消息如下所示(使用 clang 7gcc 7.5 测试):

// These are expected errors,the question is why constructor_T({})  
// is not ambiguous.

ambiguous.cpp:28:5: error: call to 'constructor_int' is ambiguous
    constructor_int({});
    ^~~~~~~~~~~~~~~
ambiguous.cpp:14:6: note: candidate function
void constructor_int(std::initializer_list<int> l) {
     ^
ambiguous.cpp:18:6: note: candidate function
void constructor_int(std::initializer_list<std::initializer_list<int>> ll) {

为什么有一个模板可以解决这里的歧义?

解决方法

constructor_int({});                // ambiguous,why?

您可以使用 initializer_list<int> 构造 initializer_list<initializer_list<int>>{}

constructor_int({{},{}});          // ambiguous,why?

您可以使用 initializer_list<int> 构造 initializer_list<initializer_list<int>>{{},{}}

试试看

initializer_list<initializer_list<int>> a={{},{}};
initializer_list<int> b={{},{}}; // aka {0,0}

所以这些很无聊。两者都可以。

但为什么模板有效?

“更专业”的规则。

当两个模板都有效时,只有更专业的一个参与重载解析。

这类似于

template<class T>
void foo(T);
template<class U>
void foo(U*);

当我打电话

foo((void*)0);

我们得到 T=void*U=void

template<class T=void*>
void foo(void*);
template<class U=void>
void foo(void*);

如果你忽略模板更专业的规则,两者都是同样好的重载

但是因为 T 可以是任何 U*U* 不能是任何 T,所以 U* 更专业。

所以 C++ 选择 U*

同样的事情发生在 initializer_list<initializer_list<T>>initializer_list<T> 更专业。