c++templateslambdainitializervariadic

Confusion about type deduction in variadic templates


I have a class with multiple constructors, one being variadic and one taking an additional callable before the variadic arguments. However, the objects I create result in overload resolutions I dont understand. Here's my snippet:

#include <iostream>
#include <functional>

struct Test 
{
    std::function<void()> func;
    
    template<class... Args>
    Test(Args&&... args)
    {
        std::cout << "Variadic" << std::endl;
    }
    
    template<class... Args>
    Test(decltype(func) newfunc, Args&&... args)
    {
        std::cout << "Variadic with a callable" << std::endl;
    }
};

int main()
{

    auto l = [](){};
    
    Test t{1,2,3}; //As expected
    
    Test t2{l, 1,2,3}; //Not as expected
    
    Test t3{{l}, 1,2,3}; //As expected, but why???
    
    //Same test, just with temporary
    Test t4{[](){}, 1,2,3}; 
    
    Test t5{{[](){}}, 1,2,3}; 
    
    return 0;
}

The output from this program (Both gcc and MSVC) is

Variadic

Variadic

Variadic with a callable

Variadic

Variadic with a callable

The first call makes perfect sense, but I would expect call 2 to result in the one taking a callable since std::function objects can be created from lambdas. However, it doesnt, but as soon as I wrap that lambda in another pair of braces in case 3, essentially converting it to an initializer_list with 1 element, the correct constructor is invoked. What I dont understand is why case 2 does not result in the 2nd overload being chosen and why making it an initializer list changes this behavior.


Solution

  • a lambda is not std::function, it is convertible_to one.

    when the compiler sees a variadic function of the form

    template<class... Args>
        Test(Args&&... args)
    ...
    template<class... Args>
        Test(std::function<void()> newfunc, Args&&... args)
    ...
    Test t4{[](){}, 1,2,3}; 
    

    the first one is a perfect fit, it doesn't require any cast, the second one requires a cast from a lambda to std::function, so the first one is chosen by overload resolution.

    in the third case,

    Test t3{{l}, 1,2,3}; 
    

    the braces {l} are calling a an implicit conversion then passing the result to the function, so the compiler has to find an overload that requires an implicit conversion, so it decides to call the std::function constructor, then do ADL, which results in the second function to be the perfect fit because it is more specialized than function 1 and no cast is required, the first one doesn't require an implicit conversion so it doesn't participate in ADL.

    refer to Implicit conversion sequence in list-initialization for how the compiler picks the best implicit conversion to do.

    you can break this by applying a concept, or use enable_if with SFINAE and is_convertible

    #include <iostream>
    #include <functional>
    #include <concepts>
    
    
    template <typename T>
    concept ConvertibleTofunc = std::convertible_to<T,std::function<void()>>;
    
    struct Test 
    {
        std::function<void()> func;
    
        template<class... Args>
        Test(Args&&... args)
        {
            std::cout << "Variadic" << std::endl;
        }
        
        template<ConvertibleTofunc Arg, class... Args>
        Test(Arg&& newfunc, Args&&... args)
        {
            std::cout << "Variadic with a callable" << std::endl;
        }
    };
    
    int main()
    {
        auto l = [](){};
    
        Test t{1,2,3};
        
        Test t2{l, 1,2,3};
        
        //Same test, just with temporary
        Test t4{[](){}, 1,2,3}; 
       
        return 0;
    }
    
    Variadic
    Variadic with a callable
    Variadic with a callable