0

I'm trying to overload a templated function but they turn out to be ambiguous. The function is used to convert a value into a user-defined type (which can be whatever.) The user provides a conversion function and, in some cases, default values or a check function. In order to detect Callables I rely upon this answer and its definition of is_callable.

(Simplified) Example below:

#include <iostream>
#include <type_traits>
#include <utility>
#include <functional>

template<class F, class...Args>
struct is_callable {
    template<class U> static auto test(U* p) -> decltype((*p)(std::declval<Args>()...), void(), std::true_type());
    template<class U> static auto test(...) -> decltype(std::false_type());

    static constexpr bool value = decltype(test<F>(0))::value;
};


class Proto {
    template <class U>
    using ConvertedParamType = typename std::decay<
        typename std::result_of<typename std::decay<U>::type&(const std::string&)>::type>::type;

public:
    /* --- (A) Ambiguous: ------- */
    template <class F, class T = ConvertedParamType<F>,
        class C, typename = typename std::enable_if<
            is_callable<F, const std::string&>::value && is_callable<C, T&>::value>::type>
    T getParamFunc(const std::string& value, F pconv_functor, C pcheck_functor) const
    {
        std::cout << "** Converting (A)!\n";
        T retval = pconv_functor(value);
        pcheck_functor(retval);
        return retval;
    }

    /* --- (B) Ambiguous: ------- */
    template <class F, class T = ConvertedParamType<F>,
        typename = typename std::enable_if<
            is_callable<F, const std::string&>::value &&
            (not is_callable<T, T&>::value)
        >::type >
    T getParamFunc(const std::string& value, F pconv_functor, T dflt_val) const 
    {
        std::cout << "** Converting (B)!\n";
        T retval = pconv_functor(value);
        return (retval > dflt_val ? retval : dflt_val);
    }
};

I (partially) understand why the compiler finds (A) and (B) ambiguous, but I can't reason whether this owes to the definition of is_callable or something else.

Would there be a way to fix this? Or a better way to address this design(*)? My sincere apologies if this is either silly or impossible, I'm still learning how to properly implement metaprogramming techniques.

Test code:

int func1(const std::string& s) {
    std::cout << "*** Converting with func1!\n";
    return std::stoi(s);
}
int main(void)
{
    auto f = [](const std::string& s) -> int {
        std::cout << "*** Converting with lambda f!\n";
        return std::stoi(s);
    };
    auto c = [](int& i) {
        i *= -1;
    };
    Proto p;
    /* OK: */
    std::cout << "Converted value: " << p.getParamFunc("123", func1) << "\n";
    std::cout << "Converted value: " << p.getParamFunc("123", f) << "\n";
    /* Error, ambiguous (both with f or func1): */
    std::cout << "Converted value: " << p.getParamFunc("123", f, 456) << "\n";
    std::cout << "Converted value: " << p.getParamFunc("123", f, c) << "\n";

    return 0;
}

(* NB: I need the user to provide its own conversion function and I want to overload getParamFunc(). My design actually has a lot of additional overloads, including ones in which boundaries are given or others in which a different check function is expected. Both the default value and the return type T should be deducible in all cases, but I think this is already sorted out.)

max66
  • 65,235
  • 10
  • 71
  • 111
Carles Araguz
  • 1,157
  • 1
  • 17
  • 37
  • This isn't minimal, read [mcve]. Of particular interest is the B overload having a default type for `T` but also a parameter of type `T` which renders the default mostly useless, and `is_callable` which doesn't look right, and – Passer By Jul 10 '18 at 11:27
  • @PasserBy Thanks for your comment (which BTW appears to be cut off.) I have tried to simplify the example even more. Why would `is_callable` be wrong...? – Carles Araguz Jul 10 '18 at 13:40
  • I think that was a half sent edit, sorry bout that. `is_callable` isn't necessarily wrong, but looks very much like a typo, I haven't looked that carefully through the entire snippet. – Passer By Jul 10 '18 at 13:50

1 Answers1

1

As pointed by Passer By, a default type for a type (T) that is deduced

template <class F, class T = ConvertedParamType<F>,
    typename = typename std::enable_if<
        is_callable<F, const std::string&>::value &&
        (not is_callable<T, T&>::value)
    >::type >
T getParamFunc(const std::string& value, F pconv_functor, T dflt_val) const;

is useless because the default type is never used.

I suppose you can deduce T (maybe you can make dflt_val a T const &) and impose that T is ConvertedParamType<F> in the following condition.

Something as

template <class F, class T,
    typename = typename std::enable_if<
        is_callable<F, const std::string&>::value &&
        std::is_same<T, ConvertedParamType<F>>::value
    >::type >
T getParamFunc(const std::string& value, F pconv_functor, T const & dflt_val) const;

If you want, you can also add the (not is_callable<T, T&>::value) test.

max66
  • 65,235
  • 10
  • 71
  • 111