1

With C++ 20 concepts we can write smth like:

#define has_method(CLASS_NAME, METHOD_NAME) \ // Can be generalized of course, but it's just to illustrate the idea
    []<typename TEMPLATE_ARG>()             \
    {                                       \
        return requires{ std::declval<TEMPLATE_ARG>().METHOD_NAME(); }; \
    }.template operator()<CLASS_NAME>()

And then use it like that:

int main()
{
    struct S{
        void call_1();
        void call_2();
        void call_3();
        void call_4();
        };
    static_assert(has_method(S, call_1)); // OK
    static_assert(has_method(S, call_2)); // OK
    static_assert(has_method(S, call_3)); // OK
    static_assert(has_method(S, call_4)); // OK
    static_assert(has_method(S, call_5)); // Error
}

Is there any way I can implement has_method macro without concepts (i.e with C++17 only)?

To make it more clear: I need a macro which can accept class and method name and I want to use such a macro inside constexpr context just like a constexpr function. Standard SFINAE implementation requires to create at least two template structures which makes it difficult (or impossible) to put all of that into one macro. So my question whether or not it is actually impossible.

Dmitry
  • 1,065
  • 7
  • 15
  • 1
    Does this answer your question? [C++ compile time check if method exists in template type](https://stackoverflow.com/questions/55191505/c-compile-time-check-if-method-exists-in-template-type) – perivesta Feb 21 '21 at 10:29
  • How would you do that? I need a macro which accepts class and method name. – Dmitry Feb 21 '21 at 10:29
  • https://stackoverflow.com/questions/257288/templated-check-for-the-existence-of-a-class-member-function#257382 – user202729 Feb 21 '21 at 10:30
  • You would need to create a structure to use "standard" SFINAE. How can you put all of that into one macro to use it inside "if constexpr"? – Dmitry Feb 21 '21 at 10:31
  • 1
    Why on earth does it have to be "in one macro"? Some of the most widely used, production quality software libraries use details in their headers that are not themselves hidden behind a macro (even though their macros use them). This requirement seems strange. – StoryTeller - Unslander Monica Feb 21 '21 at 11:15
  • By "in one macro" I mean an ability to create a "function like" something, which can return bool value (acts like a constexpr function). What's the alternative? Create a different "SFINAE-structures" each time we want to check a method existence? – Dmitry Feb 21 '21 at 11:35
  • @NicolBolas This is not a duplicate. This question seeks a solution parametrized by both the class AND the method. The linked answers can only check a presence of pre-defined, hardcoded method, that is not what the author requests here. – Quimby Feb 22 '21 at 11:38

1 Answers1

5

Yes, this is possible, though it does require a macro AFAIK, but you seem to not mind that.

We of course need some SFINAE context because that is the only place where we are allowed to write syntax errors. But creating in-place templates is tricky because local classes cannot contain them, so even if(class { /* template magic */}x; <something with x>) C++17 feature won't help much.

Since C++14 there is one feature that I am aware of which does allow defining types with templated methods inside an expression - templated lambdas.

I took inspiration from std::visit with overloaded set and SFINAE test using overloads:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
 
#define HAS_METHOD(class_type,method)                                               \
overloaded {                                                                        \
    [](auto* arg, decltype(&std::decay_t<decltype(*arg)>::method) ptr) constexpr    \
        { return true;},                                                            \
    [](auto* arg, ...) constexpr                                                    \
        { return false;}                                                            \
}((class_type*)nullptr,nullptr)                                                                         

#include <iostream>

int main()
{ 
    struct S{void call();};
    if constexpr (HAS_METHOD(S,call))
        std::cout << "S has a call\n";
    else
        std::cout << "S does not have a call\n";

    if constexpr (HAS_METHOD(S,call2))
        std::cout << "S has a call2\n";
    else
        std::cout << "S does not have a call2\n";
}

Output(Godbolt):

S has a call
S does not have a call2

Explanation

Overload-based SFINAE

Based on this answer, one can base SFINAE on overloading resolution between templated functions. Nice property of this is there is no need for specialization.

Here I used this approach for lambda's templated operator(). The code boils down to something like this:

struct overload{
    template<class T>
    bool operator()(T * arg, decltype(&std::decay_t<decltype(*arg)>::call) ptr)
    {
      return true;
    }
    template<class T>
    bool operator()(T * arg, ...)
    {
      return false;
    }
};

When we call overload{...}((S*)nullptr,nullptr), T is deduced to be S from the first parameter. This effectivelly gets rids of the templated code while still being in SFINAE context. The first (auxillary) parameter is necessary because lambdas do not have template <typename S> prior to C++20, also to obtain the type, one has to use decltype(arg). std::decay_t is required because dereferencing a pointer returns a reference and T&::call is never valid syntax.

Note that one cannot use std::declval here because the context is evaluated. Pointer it is then, we won't actually dereference it anywhere. Now

  1. If S::call is valid, the second parameter is of type "pointer to a member function with call's signature". Of course nullptr is a valid value for any pointer. Because this overload is more specific than ...(anything valid is), it is chosen and true is returned in constexpr manner.
  2. If S::call constitues a syntax error, the first overload is discarded by SFINAE, the second still matches because ... will match nullptr and the first argument could still be deduced. In that case we return false.

Overloading

To build the required set of overloads from lambdas in one expression, one can use parameter pack expansion and inheritance of methods which is exactly what this line from std::visit helper does:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };

Then the macro itself just construct a temporary instance of this class, ctor is used to initialize the base classes = pass the lambdas. After that, the temporary is immedietely called with ((S*)nullptr,nullptr) arguments.

Quimby
  • 17,735
  • 4
  • 35
  • 55