Search code examples
c++c++11variadic-templatesinitializer-listtype-deduction

Using Initializer Lists with Variadic Templates


I have the following code:

#define RETURNS(...) -> decltype((__VA_ARGS__)) { return (__VA_ARGS__); }

template <typename This, typename... Args>
auto fun(This &&this_, Args&&... args) RETURNS(this_.fun(std::forward<Args>(args)...))

For better or worse, this allows me to use fun(o, args...) and o.f(args...) interchangeably. The difficulty in using this comes when using an initializer list as an argument. E.g.

fun(obj, {1, 2, 3}); // Where obj.fun eventually takes a std::vector<int>.

This fails due to a substitution error, so Clang says. Note, obj.fun({1, 2, 3}); works.

As I understand it from other questions, this is because initializer lists don't always play nicely with template argument deduction.

The closest I have to my desired syntax is by making the initializer list more explicit. To avoid verbosity, I have the following:

template <typename T> std::initializer_list<T> il(std::initializer_list<T> &&li) { return li; }

fun(obj, il({1, 2, 3}));

Is there a way of getting my desired syntax or closer to it?


Clang's error report in my test program is:

subst.cpp:16:5: error: no matching function for call to 'fun'
    fun(x, {1, 2, 3});
    ^~~
subst.cpp:6:6: note: candidate template ignored: substitution failure [with This
      = X &, Args = <>]: too few arguments to function call, single argument 'v'
      was not specified
auto fun(This &&this_, Args&&... args) RETURNS(this_.fun(std::forward<Args>(args)...))
     ^                                                                              ~

Solution

  • You should have provided the signature of your member function fun. Let's assume that it has the following signature:

    class YourClassObj
    {
    public:
        template<typename ...Args>
        SomeType fun(Args ...args) {...}
        ...
    };
    

    Then the following call:

    fun(obj, {1, 2, 3});
    

    with a forwarding function, causes actually a non-deduced context related to failing to deduce type for {1,2,3}. As per the Standard:

    14.8.2.1 Deducing template arguments from a function call [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. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context (14.8.2.5).

    Examples from the standard:

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

    std::forward essentially does template type deduction from function call. Scott Meyers describes in his Effective Modern C++ book this situation as one of the perfect forwarding failure cases. As he said in the book, one simple workaround is to use auto:

    auto __il = {1,2,3};
    fun(obj, __il);
    

    should compile fine, since auto deduces {1,2,3} to be a std::initializer_list<int>. You can read the whole chapter for more detailed explanation.

    And by the way, the other approach with a function il returning a std::initializer_list<T> should also compile, if the original fun signature is actually the same as what I assumed (tested with gcc-4.9.1).