Search code examples
c++templatesc++11initializer-list

In C++ template copy assignment operator not compatible with initializer_list?


Consider I have such code:

#include <initializer_list>

class my_class
{
public:
    my_class() {}

    void operator = (const std::initializer_list<int>&) {} // OK
    template<typename ValueType> void operator = (const ValueType&) {} // Failed
};

int main(int argc, char* argv[])
{
    my_class instance;
    instance = {1, 2};

    return 0;
}

The first copy assignment operator could be compiled OK with instance = {1, 2}. However, the template version would failed with such error:

code.cpp:15:14: error: no viable overloaded '='
    instance = {1, 2};
    ~~~~~~~~ ^ ~~~~~~
code.cpp:3:7: note: candidate function (the implicit copy assignment operator) not viable: cannot convert initializer list argument to 'const my_class'
class my_class
      ^
code.cpp:3:7: note: candidate function (the implicit move assignment operator) not viable: cannot convert initializer list argument to 'my_class'
class my_class
      ^
code.cpp:9:39: note: candidate template ignored: couldn't infer template argument 'ValueType'
    template<typename ValueType> void operator = (const ValueType&) {}

Why the template version is not compatible with the initializer_list?


Solution

  • Because initializer list is a non-deduced context. From [temp.deduct.type]:

    The non-deduced contexts are:
    — [...]
    — A function parameter for which the associated argument is an initializer list (8.5.4) but the parameter does not have a type for which deduction from an initializer list is specified (14.8.2.1). [ Example:

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

    —end example ]

    There are some cases, however, where you can still pass in an initializer list to a template. From [temp.deduct.call]

    Template argument deduction is done by comparing each function template parameter type (call it P) with the type of the corresponding argument of the call (call it A) as described below. If P is a dependent type, 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 (8.5.4), 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.

    The examples that follow illustrate the cases in which this works:

    template<class T> void f(std::initializer_list<T>);
    f({1,2,3}); // T deduced to int
    
    template<class T, int N> void h(T const(&)[N]);
    h({1,2,3}); // T deduced to int, N deduced to 3
    
    template<class T> void j(T const(&)[3]);
    j({42}); // T deduced to int, array bound not considered
    

    So in your specific case, you could do something like:

    template <typename T>
    void operator=(std::initializer_list<T> ) { }
    

    Or:

    template <typename T, size_t N>
    void operator=(T const(&)[N]) { }
    

    Although the latter apparently doesn't compile on clang, incorrectly.