仅使用 C++17,是否可以创建一个宏,它接受类和方法名称并在类中存在这样的方法时返回 true?

Dmi*_*try 1 c++ c++17 c++20

使用 C++ 20 概念,我们可以这样写:

#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>()
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它:

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
}
Run Code Online (Sandbox Code Playgroud)

有什么方法可以在has_method没有概念的情况下实现宏(即仅使用 C++17)?

更清楚地说:我需要一个可以接受类和方法名称的宏,我想在 constexpr 上下文中使用这样的宏,就像 constexpr 函数一样。标准 SFINAE 实现需要创建至少两个模板结构,这使得很难(或不可能)将所有这些都放入一个宏中。所以我的问题是否实际上是不可能的。

Qui*_*mby 5

是的,这是可能的,尽管它确实需要宏 AFAIK,但您似乎并不介意。

我们当然需要一些 SFINAE 上下文,因为这是我们唯一允许编写语法错误的地方。但是创建就地模板很棘手,因为本地类不能包含它们,所以即使是if(class { /* template magic */}x; <something with x>)C++17 特性也无济于事。

从 C++14 开始,我知道有一个特性允许在表达式中使用模板化方法定义类型 - 模板化 lambdas。

我从std::visit使用重载的重载集合和SFINAE 测试中获得灵感:

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";
}
Run Code Online (Sandbox Code Playgroud)

输出(Godbolt)

S has a call
S does not have a call2
Run Code Online (Sandbox Code Playgroud)

解释

基于过载的 SFINAE

基于答案,可以将 SFINAE 建立在模板化函数之间的重载解析上。这个不错的特性是不需要专业化。

在这里,我将这种方法用于 lambda 的模板化operator(). 代码归结为这样的:

S has a call
S does not have a call2
Run Code Online (Sandbox Code Playgroud)

当我们调用时overload{...}((S*)nullptr,nullptr),从第一个参数T推导出来S。这有效地摆脱了模板化代码,同时仍处于 SFINAE 上下文中。第一个(辅助)参数是必需的,因为 lambdastemplate <typename S>在 C++20 之前没有,而且要获取类型,必须使用decltype(arg). std::decay_t是必需的,因为取消引用一个指针会返回一个引用并且T&::call永远不是有效的语法。

请注意,std::declval这里不能使用,因为要评估上下文。指针就是这样,我们实际上不会在任何地方取消引用它。现在

  1. 如果S::call有效,则第二个参数的类型为“指向具有call's 签名的成员函数的指针”。当然nullptr是任何指针的有效值。因为这个重载比...(任何有效的)更具体,所以它被选择并以某种方式true返回constexpr
  2. 如果S::call构成语法错误,第一个重载将被 SFINAE 丢弃,第二个仍然匹配,因为...将匹配nullptr并且仍然可以推导出第一个参数。在这种情况下,我们返回false

超载

要在一个表达式中从 lambda 构建所需的一组重载,可以使用参数包扩展和方法继承,这正是std::visithelper中的这一行所做的:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
Run Code Online (Sandbox Code Playgroud)

然后宏本身只是构造了这个类的临时实例,ctor 用于初始化基类 = 传递 lambdas。之后,临时对象会立即被((S*)nullptr,nullptr)参数调用。