Check if a class has a method with a given name but any signature
Asked Answered
S

3

7

I'm trying to find a way to simply check if a method of a given name exists in a c++ class using c++11 features, but without(!) checking the signature.

I was unable to find anything without the signature check, so I tried to use Valentin Milea's solution from here and modify it (see below), but my understanding of c++ is not deep enough to really grasp what's going on there:

#include <type_traits>

template <class C>
class HasApproxEqualMethod
{
    template <class T>
    static std::true_type testSignature(bool (T::*)(const T&, double) const);

    template <class T>
    static decltype(testSignature(&T::approx_equal)) test(std::nullptr_t);

    template <class T>
    static std::false_type test(...);

public:
    using type = decltype(test<C>(nullptr));
    static const bool value = type::value;
};

class Base {
public:
virtual ~Base();
virtual bool approx_equal(const Base& other, double tolerance) const;
};

class Derived : public Base {
public:
     // same interface as base class
    bool approx_equal(const Base& other, double tolerance) const;
};

class Other {};

static_assert(HasApproxEqualMethod<Base>().value == true, "fail Base");
static_assert(HasApproxEqualMethod<Other>().value == false, "fail Other");
 // this one fails:
static_assert(HasApproxEqualMethod<Derived>().value == true, "fail Derived");

I think the root of the problem is that my approx_equal uses base class references also in the derived class(es) and that doesn't match the signature anymore.

In the end, I want to construct a template comparison function that calls approx_equal if it exists, or something else( e.g. == for strings etc., or fabs(a-b) <= tolerance for floats and doubles). The approx_equal functions will then in turn call the template comparison for each member.

I actually got that part to work with an ugly workaround, where each class with an approx_equal method also gets a member variable const static char hasApproxEqualMethod. Then I check if that variable exists as suggested here, but that's certainly not the way to go.

Staging answered 17/1, 2019 at 13:8 Comment(1)
The answers from this question should come handy. Just replace operator() with approx_equal.Veator
R
3

The problem in your code is that you check that a class has method that accept, as first argument a constant reference to an object of the same class

template <class T>
static std::true_type testSignature(bool (T::*)(const T&, double) const);
// .......................................^...........^   same class

but inside Derived you define a method that receive an object of a different class (Base)

// ...VVVVVVV  object is Derived
class Derived : public Base {
public:
     // same interface as base class
    bool approx_equal(const Base& other, double tolerance) const;
    // .....................^^^^  method accept Base
};

A possible solution is relax the test in HasApproxEqualMethod to accept also objects of different classes

template <class T, class U>
static std::true_type testSignature(bool (T::*)(const U&, double) const);
// now class and argument are different...^...........^

This way is satisfied also

static_assert(HasApproxEqualMethod<Derived>().value == true, "fail Derived");

If you want avoid at all the signature check, you can try something similar

template <typename T>
constexpr auto haemHelper (T const &, int)
   -> decltype( &T::approx_equal, std::true_type{} );

template <typename T>
constexpr std::false_type haemHelper (T const &, long);

template <typename T>
using HasApproxEqualMethod = decltype( haemHelper(std::declval<T>(), 0) );

but, this way, HasApproxEqualMethod<T> is std::true_type also when T has a approx_equal method with a completely different signature or also when approx_equal is a simple member (a variable).

Renown answered 17/1, 2019 at 14:34 Comment(0)
B
8

What about using std::is_member_function_pointer_v (requires c++17):

// Works
static_assert(std::is_member_function_pointer_v<decltype(&Base::approx_equal)>); 
// Works
static_assert(std::is_member_function_pointer_v<decltype(&Derived::approx_equal)>); 
// Fails as expected
static_assert(!std::is_member_function_pointer_v<decltype(&Other::approx_equal)>); 

You can shorten it like this:

template<typename Class> 
constexpr bool has_approx_equal()
{
    return std::is_member_function_pointer_v<decltype(&Class::approx_equal)>; 
}

static_assert(has_approx_equal<Base>()); 
static_assert(has_approx_equal<Derived>()); 
static_assert(!has_approx_equal<Other>());

Final solution uses SFINAE so that the compilation doesn't abort when tring to evaluate &Other::approx_equal:

template<typename Class, typename Enabled = void> 
struct has_approx_equal_s
{
    static constexpr bool value = false;  
};

template<typename Class> 
struct has_approx_equal_s
<
    Class, 
    std::enable_if_t
    <
        std::is_member_function_pointer_v<decltype(&Class::approx_equal)>
    > 
> 
{
    static constexpr bool value = std::is_member_function_pointer_v<decltype(&Class::approx_equal)>; 
};

template<typename Class> 
constexpr bool has_approx_equal()
{
    return has_approx_equal_s<Class>::value; 
};

static_assert(has_approx_equal<Base>()); 
static_assert(has_approx_equal<Derived>()); 
static_assert(has_approx_equal<Other>(), "Other doesn't have approx_equal.");

SFINAE makes sure it is possible to get false value before trying to evaluate the static assertion.

Bittner answered 17/1, 2019 at 13:43 Comment(1)
Note that this approach only works if the method isn't overloaded. If you add a second approx_equal method to Derived, has_approx_equal won't detect either of them because decltype(&Class::approx_equal) becomes ambiguous. I have yet to find a workaround for this.Stallion
R
3

The problem in your code is that you check that a class has method that accept, as first argument a constant reference to an object of the same class

template <class T>
static std::true_type testSignature(bool (T::*)(const T&, double) const);
// .......................................^...........^   same class

but inside Derived you define a method that receive an object of a different class (Base)

// ...VVVVVVV  object is Derived
class Derived : public Base {
public:
     // same interface as base class
    bool approx_equal(const Base& other, double tolerance) const;
    // .....................^^^^  method accept Base
};

A possible solution is relax the test in HasApproxEqualMethod to accept also objects of different classes

template <class T, class U>
static std::true_type testSignature(bool (T::*)(const U&, double) const);
// now class and argument are different...^...........^

This way is satisfied also

static_assert(HasApproxEqualMethod<Derived>().value == true, "fail Derived");

If you want avoid at all the signature check, you can try something similar

template <typename T>
constexpr auto haemHelper (T const &, int)
   -> decltype( &T::approx_equal, std::true_type{} );

template <typename T>
constexpr std::false_type haemHelper (T const &, long);

template <typename T>
using HasApproxEqualMethod = decltype( haemHelper(std::declval<T>(), 0) );

but, this way, HasApproxEqualMethod<T> is std::true_type also when T has a approx_equal method with a completely different signature or also when approx_equal is a simple member (a variable).

Renown answered 17/1, 2019 at 14:34 Comment(0)
U
0

check if a method of a given name exists in a c++ class using c++11 features, but without(!) checking the signature.

[..]

In the end, I want to construct a template comparison function that calls approx_equal if it exists

In fact, you want to know if bool(lhs.approx_equal(rhs, some_double)) is valid, so not exact signature, but "compatible" signature. and it is use to dispatch along overloads.

So, you may use decltype and order your overloads:

// helper function to prioritize overload. bigger has more priority
template <std::size_t N> struct overload_priority : overload_priority<N - 1> {};
template <> struct overload_priority<0> {}; // Lowest priority

// member function
template <typename T>
auto generic_approx_equal_impl(const T& lhs, const T& rhs, double tolerance, overload_priority<2>)
-> decltype(bool(lhs.approx_equal(rhs, tolerance))
{
    return lhs.approx_equal(rhs, tolerance);
}

// free function
template <typename T>
auto generic_approx_equal_impl(const T& lhs, const T& rhs, double tolerance, overload_priority<2>)
-> decltype(bool(approx_equal(lhs, rhs, tolerance))
{
    return approx_equal(lhs, rhs, tolerance);
}

// fallback
template <typename T>
bool generic_approx_equal_impl(const T& lhs, const T& rhs, double tolerance, overload_priority<0>)
{
    /*..*/
    //return abs(lhs - rhs) < tolerance;
    return false;
}

template <typename T>
bool generic_approx_equal(const T& lhs, const T& rhs, double tolerance)
{
    // Call with number greater or equal to max overloads
    return generic_approx_equal_impl(lhs, rhs, tolerance, overload_priority<10>{});
}
Unscratched answered 17/1, 2019 at 17:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.