How to test if a method is const?

The reason std::is_const<decltype(&A::method)>::value doesn't work is that a const member function isn't a const (member function). It's not a top-level const in the way that it would be for const int vs int.

What we can do instead is a type trait using void_t that tests whether we can call method on a const T:

template <typename... >
using void_t = void;

template <typename T, typename = void>
struct is_const_callable_method : std::false_type { };

template <typename T>
struct is_const_callable_method<T, void_t<
    decltype(std::declval<const T&>().method())
    > > : std::true_type { };

Demo


Create a type trait to determine the const-ness of a method:

template<typename method_t>
struct is_const_method;

template<typename CClass, typename ReturnType, typename ...ArgType>
struct is_const_method< ReturnType (CClass::*)(ArgType...)>{
    static constexpr bool value = false;
};

template<typename CClass, typename ReturnType, typename ...ArgType>
struct is_const_method< ReturnType (CClass::*)(ArgType) const>{
    static constexpr bool value = true;
};

It is a lot simpler to check whether a member function can be called on a const-qualified lvalue.

template<class T>
using const_lvalue_callable_foo_t = decltype(std::declval<const T&>().foo());

template<class T>
using has_const_lvalue_callable_foo = std::experimental::is_detected<const_lvalue_callable_foo_t, T>;

Rinse and repeat, except with std::declval<const T>(), to check if said function can be called on a const-qualified rvalue. I can think of no good use cases for const && member functions, so whether there's a point in detecting this case is questionable.

Consult the current Library Fundamentals 2 TS working draft on how to implement is_detected.


It is a lot more convoluted to check whether a particular pointer-to-member-function type points to a function type with a particular cv-qualifier-seq. That requires 6 partial specializations per cv-qualifier-seq (const and const volatile are different cv-qualifier-seqs), and still can't handle overloaded member functions or member function templates. Sketching the idea:

template<class T> 
struct is_pointer_to_const_member_function : std::false_type {};

template<class R, class T, class... Args> 
struct is_pointer_to_const_member_function<R (T::*)(Args...) const> : std::true_type {};

template<class R, class T, class... Args> 
struct is_pointer_to_const_member_function<R (T::*)(Args...) const &> : std::true_type {};

template<class R, class T, class... Args> 
struct is_pointer_to_const_member_function<R (T::*)(Args...) const &&> : std::true_type {};

template<class R, class T, class... Args> 
struct is_pointer_to_const_member_function<R (T::*)(Args..., ...) const> : std::true_type {};

template<class R, class T, class... Args> 
struct is_pointer_to_const_member_function<R (T::*)(Args..., ...) const &> : std::true_type {};

template<class R, class T, class... Args> 
struct is_pointer_to_const_member_function<R (T::*)(Args..., ...) const &&> : std::true_type {};

If you want const volatile to be true too, stamp out another 6 partial specializations along these lines.


In C++20, things get a lot easier because concepts have been standardized, which subsumes the detection idiom.

Now all we need to write is this constraint:

template<class T>
concept ConstCallableMethod = requires(const T& _instance) {
    { _instance.method() }
};

ConstCallableMethod tests that the expression _instance.has_method() is well formed given that _instance is a const-reference type.

Given your two classes:

struct A {
    void method() const { }
};

struct B {
    void method() { }
};

The constraint will be true for A (ConstCallableMethod<A>) and false for B.


If you wish to also test that the return type of the method function is void, you can add ->void to the constraint like so:

template<class T>
concept ConstCallableMethodReturnsVoid = requires(const T& _instance) {
    { _instance.method() } -> void
};

If you wish to be a little more generic, you can pass in a member function pointer to the concept and test if that function pointer can be called with a const instance (although this gets a little less useful when you have overloads):

template<class T, class MemberF>
concept ConstCallableMemberReturnsVoid = requires(const T& _instance, MemberF _member_function) {
    { (_instance.*_member_function)() } -> void
};

You'd call it like so:

ConstCallableMemberReturnsVoid<A, decltype(&A::method)>

This allows for some other theoretical class like C, that has a const method, but it's not named method:

struct C
{
    void foobar() const{}
};

And we can use the same concept to test:

ConstCallableMemberReturnsVoid<C, decltype(&C::foobar)>

Live Demo