How can I prevent implicit conversion among `std::function`s with differenent argument types?
Asked Answered
S

4

7

I'm trying to bind some ta-lib functions and then callback.

Here is the simplified sample code:

#include <functional>
#include <type_traits>
#include <cstdint>

struct DataChunk {
    // ...
};
typedef uint64_t idx_t;

template <typename LookbackArgType> // int, double
struct talib_traits {
    using talib_lookback_t = std::function<int(LookbackArgType)>;
    using talib_function_t = std::function<void(DataChunk &, void *, idx_t, idx_t, LookbackArgType)>;
};

template <>
struct talib_traits<void> {
    using talib_lookback_t = std::function<int()>;
    using talib_function_t = std::function<void(DataChunk &, void *, idx_t, idx_t)>;
};

struct X {
    talib_traits<int>::talib_lookback_t talib_lookback_int = nullptr;
    talib_traits<int>::talib_function_t talib_function_int = nullptr;

    talib_traits<double>::talib_lookback_t talib_lookback_double = nullptr;
    talib_traits<double>::talib_function_t talib_function_double = nullptr;

    talib_traits<void>::talib_lookback_t talib_lookback_void = nullptr;
    talib_traits<void>::talib_function_t talib_function_void = nullptr;

    explicit X(talib_traits<int>::talib_lookback_t talib_lookback, talib_traits<int>::talib_function_t talib_function)
        : talib_lookback_int(talib_lookback), talib_function_int(talib_function) {
    }
    explicit X(talib_traits<double>::talib_lookback_t talib_lookback,
               talib_traits<double>::talib_function_t talib_function)
        : talib_lookback_double(talib_lookback), talib_function_double(talib_function) {
    }
    explicit X(talib_traits<void>::talib_lookback_t talib_lookback, talib_traits<void>::talib_function_t talib_function)
        : talib_lookback_void(talib_lookback), talib_function_void(talib_function) {
    }
};

int main() {
    constexpr bool lookback_is_same =
        std::is_same<talib_traits<int>::talib_lookback_t, talib_traits<double>::talib_lookback_t>::value;
    constexpr bool function_is_same =
        std::is_same<talib_traits<int>::talib_function_t, talib_traits<double>::talib_function_t>::value;
    static_assert(!lookback_is_same && !function_is_same);

    X x([](void) { return 0; }, [](DataChunk &, void *, idx_t, idx_t) {}); // okay

    // ambiguous: more than one instance of constructor "X::X" matches the argument list, int or double?
    X y([](int) { return 0; }, [](DataChunk &, void *, idx_t, idx_t, int) {});
}

How can I make them unambiguous, that is, something like preventing std::function<int(int)>/int (*)(int) from being converted to std::function<int(double)>?

I tried to prefix explicit keyword to the constructors, still it doesn't prevent ambiguousness.

Snappish answered 13/2, 2023 at 9:13 Comment(6)
Which compiler and its version?Kendry
@Kendry gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0. I tried -std=c++[11,14,17,20], all don't work.Snappish
The issue is you are trying to pass a lambda to a function which expects a std::function. If I assign the lambda to a std::function variable, it works. godbolt.org/z/o8so5ncvKKendry
you should include the complete compiler error message in the questionParenthesis
Please always tray provide minimal reproducible example, here is example for your case! This way your question is easier to understand and easier to find solution.Wilbertwilborn
Note that if you use function pointers, instead of std::function, then almost all conversions are forbidden. I assume that you've ruled out that option because you want stateful lambdas without static variables?Empyema
S
5

Letting the appropriate std::function objects be constructed implicitly obviously fails – alternatives (explicitly construct std::function objects or factory functions) are a bit wordy at the user side (i.e. when constructing the X objects). If you prefer offering more comfort there you need to invest more effort at the implementation; one possible approach there is making the constructor a template accepting arbitrary types and apply static type checks to determine if the right parameters have been passed to. This might look as follows. At first we need some further traits to be able to determine which parameter the functions passed to actually accept:

template <typename T>
struct FunctionTraits;
template <typename T>
struct FunctionTraits<std::function<int(T)>>
{
    using ParameterType = T;
};
template <typename T>
struct FunctionTraits<std::function<void(DataChunk &, void *, idx_t, idx_t, T)>>
{
    using ParameterType = T;
};
template <>
struct FunctionTraits<std::function<int()>>
{
    using ParameterType = void;
};
template <>
struct FunctionTraits<std::function<void(DataChunk &, void *, idx_t, idx_t)>>
{
    using ParameterType = void;
};
template <typename T>
using ParameterType
= typename FunctionTraits<decltype(std::function(std::declval<T>()))>::ParameterType;

Now we can use these in a template constructor:

struct X
{
   // ...

    template <typename Lookback, typename Function>
    X(Lookback l, Function f)
    {
        using PType = ParameterType<decltype(l)>;
        static_assert(std::is_same_v<PType, ParameterType<decltype(f)>>);
        if constexpr(std::is_same_v<PType, int>)
        {
            talib_lookback_int = l;
            talib_function_int = f;
        }
        else if constexpr(std::is_same_v<PType, double>)
        {
            talib_lookback_double = l;
            talib_function_double = f;
        }
        else if constexpr(std::is_same_v<PType, void>)
        {
            talib_lookback_void = l;
            talib_function_void = f;
        }
    }
}

The code will fail if non-matching functions are passed (either with differing ParamType or not even matching the signatures of the std::function types in question, otherwise elegantly create the objects intended via ordinary constructor calls, see demonstration on godbolt.

Spigot answered 13/2, 2023 at 11:21 Comment(3)
Perhaps there could even be a default fallback for non-matching functions.Hephzibah
@Hephzibah And what should that one do? struct X is only designed for matching functions, and we'd turn compile time errors into runtime ones that way, which likely is not desirable...Spigot
Okay, probably makes better sense to have a compile time error :-)Hephzibah
T
7

I don't think you can prevent a std::function<int(double)> from being constructible from a int(int) lambda. That's just how std::function is defined – it is constructible from any callable that is callable with the std::functions's arg types whose return type is implicitly convertible to the desired return type.

You can avoid this by turning your lambda into a std::function at the callsite. You don't need to specify the type arguments for std::function it this case:

X y(std::function([](int) { return 0; }),
    [](DataChunk &, void *, idx_t, idx_t, int) {});

Now the constructor that takes std::function<int(int)> doesn't require an implicit conversion of the first argument anymore, while the std::function<int(double)> variant still does. This is enough of a tiebraker for the call to no longer be ambiguous.

One would hope that the extra conversion of the return type needed when turning your lambda into a std::function<int(double)> vs a std::function<int(int)> would already be enough of a tiebreaker. But both these conversions use the same constructor of std::function and the type conversion happens within that constructor. The tie-breaking algorithm doesn't look inside the constructor, so both cases look the same to it.


Alternatively, if you turn your constructor into a templated factory function, you can explicitly specify the type (godbolt):

struct X {
    ...

    template <typename T>
    static X create(typename talib_traits<T>::talib_lookback_t talib_lookback,
             typename talib_traits<T>::talib_function_t talib_function) {
        X out;
        if constexpr(std::is_same_v<T, int>) {
            out.talib_lookback_int = talib_lookback;
            out.talib_function_int = talib_function;
        } else if constexpr(std::is_same_v<T, double>) {
            out.talib_lookback_double = talib_lookback;
            out.talib_function_double = talib_function;
        } else {
            static_assert(std::is_same_v<T, void>);
            out.talib_lookback_void = talib_lookback;
            out.talib_function_void = talib_function;
        }
        return out;
    }
    
private:
    X(){};
};

int main() {
    X x = X::create<void>([](void) { return 0; }, [](DataChunk &, void *, idx_t, idx_t) {}); // okay
    X y = X::create<int>([](int){ return 0; }, [](DataChunk &, void *, idx_t, idx_t, int) {});
    X z = X::create<double>([](double){ return 0; }, [](DataChunk &, void *, idx_t, idx_t, double) {});
}
Transcript answered 13/2, 2023 at 9:54 Comment(0)
S
5

Letting the appropriate std::function objects be constructed implicitly obviously fails – alternatives (explicitly construct std::function objects or factory functions) are a bit wordy at the user side (i.e. when constructing the X objects). If you prefer offering more comfort there you need to invest more effort at the implementation; one possible approach there is making the constructor a template accepting arbitrary types and apply static type checks to determine if the right parameters have been passed to. This might look as follows. At first we need some further traits to be able to determine which parameter the functions passed to actually accept:

template <typename T>
struct FunctionTraits;
template <typename T>
struct FunctionTraits<std::function<int(T)>>
{
    using ParameterType = T;
};
template <typename T>
struct FunctionTraits<std::function<void(DataChunk &, void *, idx_t, idx_t, T)>>
{
    using ParameterType = T;
};
template <>
struct FunctionTraits<std::function<int()>>
{
    using ParameterType = void;
};
template <>
struct FunctionTraits<std::function<void(DataChunk &, void *, idx_t, idx_t)>>
{
    using ParameterType = void;
};
template <typename T>
using ParameterType
= typename FunctionTraits<decltype(std::function(std::declval<T>()))>::ParameterType;

Now we can use these in a template constructor:

struct X
{
   // ...

    template <typename Lookback, typename Function>
    X(Lookback l, Function f)
    {
        using PType = ParameterType<decltype(l)>;
        static_assert(std::is_same_v<PType, ParameterType<decltype(f)>>);
        if constexpr(std::is_same_v<PType, int>)
        {
            talib_lookback_int = l;
            talib_function_int = f;
        }
        else if constexpr(std::is_same_v<PType, double>)
        {
            talib_lookback_double = l;
            talib_function_double = f;
        }
        else if constexpr(std::is_same_v<PType, void>)
        {
            talib_lookback_void = l;
            talib_function_void = f;
        }
    }
}

The code will fail if non-matching functions are passed (either with differing ParamType or not even matching the signatures of the std::function types in question, otherwise elegantly create the objects intended via ordinary constructor calls, see demonstration on godbolt.

Spigot answered 13/2, 2023 at 11:21 Comment(3)
Perhaps there could even be a default fallback for non-matching functions.Hephzibah
@Hephzibah And what should that one do? struct X is only designed for matching functions, and we'd turn compile time errors into runtime ones that way, which likely is not desirable...Spigot
Okay, probably makes better sense to have a compile time error :-)Hephzibah
T
3

Inspired by Aconcagua's clever answer, you could use templates and std::enable_if to disable the unwanted overload:

template<typename F, typename R, typename... Args>
constexpr bool is_functype =
    std::is_same_v<decltype(std::function(std::declval<F>())),
                   std::function<R(Args...)>>;

struct X {
    talib_traits<int>::talib_lookback_t talib_lookback_int = nullptr;
    talib_traits<int>::talib_function_t talib_function_int = nullptr;

    talib_traits<double>::talib_lookback_t talib_lookback_double = nullptr;
    talib_traits<double>::talib_function_t talib_function_double = nullptr;

    talib_traits<void>::talib_lookback_t talib_lookback_void = nullptr;
    talib_traits<void>::talib_function_t talib_function_void = nullptr;

    template<typename F, std::enable_if_t<is_functype<F, int, int>, int> = 0>
    X(F talib_lookback,
      talib_traits<int>::talib_function_t talib_function)
    : talib_lookback_int(talib_lookback),
      talib_function_int(talib_function)
    {}

    template<typename F, std::enable_if_t<is_functype<F, int, double>, int> = 0>
    X(F talib_lookback,
      talib_traits<double>::talib_function_t talib_function)
    : talib_lookback_double(talib_lookback),
      talib_function_double(talib_function)
    {}
};

godbolt

Transcript answered 13/2, 2023 at 11:59 Comment(0)
W
1

Here is my approach. Basically use templates to catch anything that does not exactly match regular overloads and apply SFINAE to cherry-pick a proper lambda argument:

struct X {

    explicit X(std::function<int()> f)
    {
        LOG(f());
    }

    explicit X(std::function<int(int)> f)
    {
        LOG(f(3));
    }

    explicit X(std::function<int(double)> f)
    {
        LOG(f(1.322));
    }

    template <typename T, int (T::*)(int) const = &T::operator()>
    explicit X(T&& f)
        : X(std::function<int(int)> { std::forward<T>(f) })
    {
        LOG(0);
    }

    template <typename T, int (T::*)(int) = &T::operator()>
    explicit X(T&& f)
        : X(std::function<int(int)> { std::forward<T>(f) })
    {
        LOG(0);
    }

    template <typename T, int (T::*)(double) const = &T::operator()>
    explicit X(T&& f)
        : X(std::function<int(double)> { std::forward<T>(f) })
    {
        LOG(0);
    }

    template <typename T, int (T::*)(double) = &T::operator()>
    explicit X(T&& f)
        : X(std::function<int(double)> { std::forward<T>(f) })
    {
        LOG(0);
    }
};

Note I've provided extra overloads to cover mutable lambdas.

Live demo

Wilbertwilborn answered 13/2, 2023 at 12:27 Comment(2)
Note that this still triggers the original error when a function pointer is passed,Transcript
In such case more overloads are needed godbolt.org/z/bjh4adYej - sadly in makes this more boiler plate.Wilbertwilborn

© 2022 - 2024 — McMap. All rights reserved.