Home > Back-end >  how to call member function if it exists, otherwise free function?
how to call member function if it exists, otherwise free function?

Time:09-04

I've got various classes:

struct foo final { std::string toString() const { return "foo"; } };

struct bar final { };
std::string toString(const bar&) { return "<bar>"; }

struct baz final { std::string toString() const { return "baz"; } };
std::string toString(const baz& b) { return "<"   b.toString()   ">"; }

struct blarf final {};

Some have a member a function toString() (foo), some have just a toString() free function (bar), some have both (baz), and some have neither (blarf).

How can I call these in order of preference: 1) member function (if it exists), 2) free function (if it exists), and 3) otherwise (no member or free toString()) a utility routine (toString_()).

I've tried this:

// https://stackoverflow.com/questions/1005476/how-to-detect-whether-there-is-a-specific-member-variable-in-class/1007175#1007175
namespace details
{
    template<typename T>
    inline std::string toString_(const T& t)
    {
        const void* pV = &t;
        return "#"   std::to_string(reinterpret_cast<size_t>(pV));
    }

    // https://stackoverflow.com/a/9154394/8877
    template<typename T>
    inline auto toString_imp(const T& obj, int) -> decltype(obj.toString(), std::string())
    {
        return obj.toString();
    }

    template<typename T>
    inline auto toString_imp(const T& obj, long) -> decltype(toString(obj), std::string())
    {
        return toString(obj);
    }

    template<typename T>
    inline auto toString_imp(const T& obj, long long) -> decltype(toString_(obj), std::string())
    {
        return toString_(obj);
    }

    template<typename T>
    inline auto toString(const T& obj) -> decltype(toString_imp(obj, 0), std::string())
    {
        return toString_imp(obj, 0);
    }
}

namespace str
{
    template<typename T>
    std::string toString(const T& t)
    {
        return details::toString(t);
    }
}

But that generates a compiler error:

int main()
{
    const foo foo;
    std::cout << str::toString(foo) << "\n"; // "foo"

    const bar bar;
    std::cout << str::toString(bar) << "\n"; // "<bar>"

    const baz baz;
    std::cout << str::toString(baz) << "\n"; // "baz"

    const blarf blarf;
    std::cout << str::toString(blarf) << "\n"; // "#31415926539"
}

I'm using C 14 (no need to work with C 11).

1> error C2672: 'details::toString': no matching overloaded function found
1> message : could be 'unknown-type details::toString(const T &)'
1> message : Failed to specialize function template 'unknown-type details::toString(const T &)'
1> message : see declaration of 'details::toString'
1> message : With the following template arguments:
1> message : 'T=T'
1> message : see reference to function template instantiation 'std::string str::toString<bar>(const T &)' being compiled
1>        with
1>        [
1>            T=bar
1>        ]

CodePudding user response:

So you have three overloads of detail::toString_imp:

auto toString_imp(const T& obj, int) -> decltype(obj.toString(), std::string())
auto toString_imp(const T& obj, long) -> decltype(toString(obj), std::string())
auto toString_imp(const T& obj, long long) -> decltype(toString_(obj), std::string())

That you call with toString_imp(obj, 0)

The problem is that conversion from 0 (an int) to long or long long needs the same number of conversions (a single one), so neither the second nor third overload beats the other.

You can fix this with a worse conversion, like the ellipses conversion:

auto toString_imp(const T& obj, int) -> decltype(obj.toString(), std::string())
auto toString_imp(const T& obj, long) -> decltype(toString(obj), std::string())
auto toString_imp(const T& obj, ...) -> decltype(toString_(obj), std::string())

Or making another helper:

auto toString_imp2(const T& obj, int) -> decltype(toString(obj), std::string())
auto toString_imp2(const T& obj, long) -> decltype(toString_(obj), std::string())

auto toString_imp(const T& obj, int) -> decltype(obj.toString(), std::string())
auto toString_imp(const T& obj, long) { return toString_imp2(obj, 0); }

Or if you find your self ever having more than 3 cases, you can use this class:

template<int N> struct priority : priority<N-1> {};
template<> struct priority<0> {};

auto toString_imp(const T& obj, priority<2>) -> decltype(obj.toString(), std::string())
auto toString_imp(const T& obj, priority<1>) -> decltype(toString(obj), std::string())
auto toString_imp(const T& obj, priority<0>) -> decltype(toString_(obj), std::string())

// call: toString_imp(obj, priority<2>{})

(Increasing the number in priority when you have more overloads. It works since the lower the priority number, the more base class conversions you need)

  • Related