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

2

10

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;
}
Reciprocity answered 14/3, 2018 at 14:43 Comment(13)
Thanks for the live MCVE!Catron
lambda are not std::function.Leto
I had to reread the constructor parameters 3 times before I noticed a_type and b_type are different.Magenta
Short answer is that both constructors can be used because the fifth overload of std::function.Magenta
A minimal reproducible example will actually look more like thisChristiniachristis
@SombreroChicken: And that std::tuple<long long,int> and std::tuple<std::sizt_t,int> are constructible from each other.Leto
Which constructor do you want to use?Haslet
I would like to use the one with b_typeReciprocity
@Leto Good addition, thanks.Magenta
@SobreroChicken, but i thought the using the keyword explicit should prevent the conversion from std::tuple<long long,int> to std::tuple<long long,int>Reciprocity
Another workaround alternative is to not use a tuple for the b_type, but instead roll your own. using b_type = struct { std::size_t a; std::size_t b; };Disparity
thanks Eljay, however I want to keep the structure considering how all these elements fit together. Also, I give a particular example with 2 types, but my original class uses a variadic list of types. At that point, the struct solution is far less workable and requires doubling the code (one for use with a_type and one with b_type)Reciprocity
@Reciprocity "... should prevent the conversion from std::tuple<long long,int> to std::tuple<long long,int>" It does if you change that converting constructor to a one argument constructor callable only by direct initialization, by marking it explicit, or if you remove that implicit conversion operator, whichever it is that perform the conversion (I haven't checked and don't want to, as it's irrelevant for the point I'm making). But then of course std::tuple is not a customization point, you can't change it.Provo
C
1

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

Christiniachristis answered 14/3, 2018 at 15:14 Comment(3)
I've been trying to put this together for VC++, but std::is_invocable_v isn't available and I can't get std::_Is_invocable_r to work. Any suggestions?Reciprocity
@Reciprocity It seems like MSVC doesn't have this yet. You may try implement it yourself. A possible implementation is to first implement invoke and SFINAE on whether the invocation is successfulChristiniachristis
I tried to take a stab at it from a different angle (mostly since I had the structures for it already built), but it doesn't seem to be working. I posted it up as another questions because I feel it might be sufficiently different, in case you want to take a peek: hereReciprocity
M
4

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
    };
}
Magenta answered 14/3, 2018 at 14:57 Comment(1)
I was hoping for some solution that mucks around with class A, but this will have to do for now. Thanks!Reciprocity
C
1

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

Christiniachristis answered 14/3, 2018 at 15:14 Comment(3)
I've been trying to put this together for VC++, but std::is_invocable_v isn't available and I can't get std::_Is_invocable_r to work. Any suggestions?Reciprocity
@Reciprocity It seems like MSVC doesn't have this yet. You may try implement it yourself. A possible implementation is to first implement invoke and SFINAE on whether the invocation is successfulChristiniachristis
I tried to take a stab at it from a different angle (mostly since I had the structures for it already built), but it doesn't seem to be working. I posted it up as another questions because I feel it might be sufficiently different, in case you want to take a peek: hereReciprocity

© 2022 - 2024 — McMap. All rights reserved.