This is impossible in the general case, but possible in however many specific cases you want to spell out.
An initializer list has no type. The only way you can deduce a type for it (as in, separate from having a default template argument) is that we have two special cases spelled out in [temp.deduct.call]/1:
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 independently, taking P′
as separate function template parameter types P′i
and the i
th initializer element as the corresponding argument. 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.type]).
This is the rule that lets the following work:
template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }
static_assert(f({1, 2, 3}) == 1);
But that isn't enough to get this to work:
static_assert(f({{1, 2}, {3, 4}}) == 1); // ill-formed (no matching call to f)
Because the rule is - okay, we can strip one layer of initializer_list
but then we have to deduce the elements. And once we strip one layer of initializer list, we're trying to deduce T
from {1, 2}
and that fails - we can't do that.
But we know how to deduce something from {1, 2}
- that's this same rule. We just have to do it again:
template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }
template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<T>>) { return 2; }
static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
and again:
template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }
template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<T>>) { return 2; }
template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<std::initializer_list<T>>>) { return 3; }
static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
static_assert(f({{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}) == 3);
The same way we have the carve-out for std::initializer_list<T>
, we also have the carve-out for T[N]
. That works the same way, just a bit less typing:
template <typename T, size_t N>
constexpr auto f(T(&&)[N]) -> int { return 1; }
template <typename T, size_t N1, size_t N2>
constexpr auto f(T(&&)[N1][N2]) { return 2; }
template <typename T, size_t N1, size_t N2, size_t N3>
constexpr auto f(T(&&)[N1][N2][N3]) { return 3; }
static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
static_assert(f({{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}) == 3);