Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
}
like image 624
AOK Avatar asked Mar 14 '18 14:03

AOK


2 Answers

The reason it doesn't work is because a lambda is not a std::function and so the compiler tries to create one using the fifth overload of the constructor. The problem is that both of your A constructors can be used because of this conversion and the reason that std::tuple<long long,int> and std::tuple<std::size_t,std::size_t> are constructible from each other makes this ambigious for the compiler what constructor to pick.

What you could do is explicitly cast to the desired std::function (MCVE of @PasserBy used in comments), like this:

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

template<typename data_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>;

    A(const std::function<data_type(a_type)>&)
    {
        std::cout << "idx_type" << std::endl;
    }
    A(const std::function<data_type(b_type)>&)
    {
        std::cout << "point_type" << std::endl;
    }
};

int main()
{
    std::function<double(std::tuple<long long, int>)> func = [](auto p) -> double { return 1; };
    auto c = A<double, long long, int>{
        func
    };
}
like image 73
Hatted Rooster Avatar answered Oct 10 '22 00:10

Hatted Rooster


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

like image 20
Passer By Avatar answered Oct 10 '22 01:10

Passer By