19

When constructing an std::initializer_list<U> explicitly, can the template argument (U) be deduced (using class template argument deduction (CTAD), for example)?

In other words, I know that the following statements are valid:

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};

but are the following statements also valid?

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

Compilers disagree on whether the template argument of std::initializer_list can be deduced:

#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
}

(See this example on Compiler Explorer.)

Compilers tested:

  • Clang version 7.0.0 with -std=c++17 -stdlib=libc++ and with -std=c++17 -stdlib=libstdc++
  • GCC version 8.3 with -std=c++17
  • MSVC version 19.16 with /std:c++17
Christophe
  • 68,716
  • 7
  • 72
  • 138
strager
  • 88,763
  • 26
  • 134
  • 176

2 Answers2

10

Clang is the only compiler that is correct. Yes, really.

When the compiler sees a template name without template parameters, it has to look at the deduction guides of the template and apply them to the arguments in the braced-init-list. initializer_list doesn't have any explicit deduction guides, so it uses the available constructors.

The only publicly accessible constructors that an initializer_list has are its copy/move constructors and its default constructor. Creating a std::initializer_list from a braced-init-list isn't done through publicly accessible constructors. It's done through list-initialization, which is a compiler-only process. Only the compiler can perform the sequence of steps needed to build one.

Given all of this, it should not be possible to use CTAD on initializer_lists, unless you're copying from an existing list. And that last part is probably how the other compilers make it work in some cases. In terms of deduction, they may deduce the braced-init-list as an initializer_list<T> itself rather than as a sequence of parameters to apply [over.match.list] to, so the deduction guide sees a copy operation.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 1
    Do you think this should be allowed? Because it seems like an oversight to me. – Rakete1111 Mar 17 '19 at 13:57
  • 4
    I'm not convinced. You apply [over.match.class.deduct], which forms - among other things - a function template from the copy deduction candidate. That function template is going to look like `template auto X(std::initializer_list) -> std::initializer_list;` Then [over.match.class.deduct] says to go to [over.match.list] using the provided initializer `{1, 2, 3}` treating the function template as a constructor template of some hypothetical class type being initialized. If you do that you'd deduce `T = int`. – T.C. Mar 17 '19 at 16:20
  • @T.C., what you're saying makes sense. If I add the following deduction guide (which should be identical to the copy deduction candidate), Clang now compiles every line in my test program (like MSVC): `namespace std { template initializer_list(std::initializer_list) -> initializer_list; }` – strager Mar 17 '19 at 16:54
  • @T.C.: "*That function template is going to look like template auto X(std::initializer_list) -> std::initializer_list;*" If that's how it supposed to work, that would explain why Clang gets it wrong. That is an initializer-list constructor, which has priority in [over.match.list]. But if Clang's deduction implementation doesn't treat it as an initializer-list constructor, or if its deduction implementation doesn't apply the rules of [over.match.list], I could see it missing that guide. It would presumably be looking for a 3-element constructor. – Nicol Bolas Mar 17 '19 at 18:21
3

This is a Clang bug, as concluded in the comment of the never fixed answer of Nicol Bolas. To summarize, as any class type, std::initializer_list has a compiler provided deduction guide [over.match.class.deduct]§1.3

An additional function template derived as above from a hypothetical constructor C(C), called the copy deduction candidate.

Which means std::initializer_list has this deduction guide implicitly declared by the compiler:

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

When deducing T for this deduction guide with a non empty initalizer list argument, the following rule of the standard is applied [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 ith initializer element as the corresponding argument.

So T should be deduced to int and so class template argument deduction shall succeed.

Disclaimer: I am not a Clang advocate...

Oliv
  • 17,610
  • 1
  • 29
  • 72