Search code examples
c++templatesvariadic-templatesparameter-pack

Why doesn't parameter pack expand to correct type?


I have a somewhat contrived piece of code:

#include <functional>

template <typename... ARGs>
auto construct1(std::function<void(ARGs...)> p, const ARGs &...args) {}

template < typename... ARGs>
auto construct2(std::function<void(int)>     p, const ARGs &...args) {}

int main() {
    auto p = [](int) {};
    construct1<int>(p, 0);
    construct2<int>(p, 0);
    return 0;
}

Why is the compiler struggling to work out that ARGs... = { int } in the first case? The code compiles okay if I assist the compiler by using std::function<void(int)> in the signature (second case). In both cases, the compiler has no trouble deducing that const ARGs&... should be const int&. Using C++17.

GCC:

main.cpp: In function ‘int main()’:
main.cpp:11:25: error: no matching function for call to ‘construct1<int>(main()::<lambda(int)>&, int)’
   11 |     construct1<int>(p, 0);
      |                         ^
main.cpp:4:6: note: candidate: ‘template<class ... ARGs> auto construct1(std::function<void(ARGs ...)>, const ARGs& ...)’
    4 | auto construct1(std::function<void(ARGs...)> p, const ARGs &...args) {}
      |      ^~~~~~~~~~
main.cpp:4:6: note:   template argument deduction/substitution failed:
main.cpp:11:25: note:   ‘main()::<lambda(int)>’ is not derived from ‘std::function<void(ARGs ...)>’
   11 |     construct1<int>(p, 0);
      |  

Clang:

main.cpp:11:5: error: no matching function for call to 'construct1'
    construct1<int>(p, 0);
    ^~~~~~~~~~~~~~~
main.cpp:4:6: note: candidate template ignored: could not match 'function<void (int, type-parameter-0-0...)>' against '(lambda at main.cpp:10:14)'
auto construct1(std::function<void(ARGs...)> p, const ARGs &...args) {}
     ^
1 error generated.

Solution

  • The problem is, construct1 is taking std::function but you're passing lambda. When you make parameter as type std::function<void(ARGs...)>, template argument deduction is performed to deduce ARGs on the function parameter p (even template parameter pack is explicitly specified with template arguments it may be extended by template argument deduction if there are additional arguments), which fails because implicit conversion won't be considered in deduction.

    Type deduction does not consider implicit conversions (other than type adjustments listed above): that's the job for overload resolution, which happens later.

    You can use std::type_identity (since C++20) to exclude p from deduction (see non-deduced contexts), e.g.

    template <typename... ARGs>
    auto construct1(std::function<void(std::type_identity_t<ARGs>...)> p, const ARGs &...args) {}
    //                                 ^^^^^^^^^^^^^^^^^^^^^    ^
    

    LIVE


    PS: Before C++20 you can make your won type_identity easily as:

    template< class T >
    struct type_identity {
        using type = T;
    };
    template< class T >
    using type_identity_t = typename type_identity<T>::type;