c++templatesimplicit-conversionexplicit-constructor

Why is the constructor in this C++ code ambiguous and how do I fix it?


In the below code, the compiler can't figure out which constructor I want to use. Why, and how do I fix this? (Live example)

#include <tuple>
#include <functional>
#include <iostream>

template<typename data_type, typename eval_type, typename Type1, typename Type2>
class A
{
public:
    using a_type = std::tuple<Type1, Type2>;
    using b_type = std::tuple<std::size_t,std::size_t>;

    inline explicit constexpr A(const std::function<data_type(a_type)>& Initializer,
        const std::function<eval_type(data_type)>& Evaluator,
        const Type1& elem1, const Type2& elem2)
    {
        std::cout << "idx_type" << std::endl;
    }
    inline explicit constexpr A(const std::function<data_type(b_type)>& Initializer,
        const std::function<eval_type(data_type)>& Evaluator,
        const Type1& elem1, const Type2& elem2)
    {
        std::cout << "point_type" << std::endl;
    }
};

int main()
{
    int a = 1;
    long long b = 2;
    auto c = A<double, double, long long, int>{
        [](std::tuple<long long,int> p)->double { return 1.0*std::get<0>(p) / std::get<1>(p); },
        [](double d)->double { return d; }, b,a
        };

    return 0;
}

Solution

  • As @SombreroChicken mentioned, std::function<R(Args...)> has a constructor that allows any callable object c to initialize it, as long as c(Args...) is valid and returns something convertible to R.

    To fix it, you may use some SFINAE machinery

    #include <tuple>
    #include <functional>
    #include <iostream>
    #include <type_traits>
    
    template<typename data_type, typename Type1, typename Type2>
    class A
    {
        template<typename T>
        struct tag
        {
            operator T();
        };
    
    public:
        using a_type = std::tuple<Type1, Type2>;
        using b_type = std::tuple<std::size_t,std::size_t>;
    
        template<typename C, std::enable_if_t<std::is_invocable_v<C, tag<b_type>>>* = nullptr>
        A(C&& initializer)
        {
            std::cout << "size_t" << std::endl;
        }
    
        template<typename C, std::enable_if_t<std::is_invocable_v<C, tag<a_type>>>* = nullptr>
        A(C&& initializer)
        {
            std::cout << "other" << std::endl;
        }
    };
    
    int main()
    {
        auto c = A<double, long long, int>{
            [](std::tuple<long long, int> p) -> double { return 1; }
        };
    
        auto c2 = A<double, long long, int>{
            [](std::tuple<std::size_t, std::size_t>) -> double { return 2; }  
        };
    }
    

    Live

    Here, we turn off the constructor if the callable can be called with b_type or a_type respectively. The extra indirection through tag is there to disable the conversion between tuples of different types