【问题标题】:Overload of variadic template function with another template用另一个模板重载可变参数模板函数
【发布时间】:2021-08-16 19:21:36
【问题描述】:

我试图弄清楚如何使用“更专业”的可变参数函数模板“重载”可变参数函数模板。例如:

#include <iostream>

template <typename... ARGS_>
void foo(void(*fn)(ARGS_...)) {
    std::cout << "Generic function pointer foo" << std::endl;
}

struct Test {
    
};

template <typename... ARGS_>
void foo(void(*fn)(ARGS_..., Test*)) {
    std::cout << "Test function pointer foo" << std::endl;
}


void test1(int a, int b) {
    std::cout << "test1()" << std::endl;
}

void test2(int a, int b, Test* x) {
    std::cout << "test2()" << std::endl;
}

int main() {
    foo(&test1);
    foo(&test2);
    
    return 0;
}

这段代码的输出是:

Generic function pointer foo
Generic function pointer foo

而不是:

Generic function pointer foo
Test function pointer foo

如我所愿。

从概念上讲,我试图指出“如果您有任何类型的参数,其中最后一个是 Test*,则使用模板方法 A,如果最后一个类型不是 Test*,则使用模板方法 B。”

完成这种行为的正确方法是什么?

【问题讨论】:

    标签: c++ templates


    【解决方案1】:

    基于最后一个参数包参数的 SFINAE 重载

    您可以根据可变参数包中的最后一个类型是否为Test* 添加互斥重载:

    #include <type_traits>
    
    template <typename... Ts>
    using last_t = typename decltype((std::type_identity<Ts>{}, ...))::type;
    
    struct Test {};
    
    template <
        typename... ARGS_,
        std::enable_if_t<!std::is_same_v<last_t<ARGS_...>, Test *>> * = nullptr>
    void foo(void (*fn)(ARGS_...)) {
      std::cout << "Generic function pointer foo" << std::endl;
    }
    
    template <
        typename... ARGS_,
        std::enable_if_t<std::is_same_v<last_t<ARGS_...>, Test *>> * = nullptr>
    void foo(void (*fn)(ARGS_...)) {
      std::cout << "Test function pointer foo" << std::endl;
    }
    
    // Special case for empty pack (as last_t<> is ill-formed)
    void foo(void (*fn)()) { std::cout << "Zero args" << std::endl; }
    

    将 C++20 的 std::type_identity 用于 last_t 转换特征。

    用作:

    void test1(int, int b) {}
    void test2(int, int b, Test *x) {}
    void test3(Test *) {}
    void test4() {}
    
    int main() {
      foo(&test1); // Generic function pointer foo
      foo(&test2); // Test function pointer foo
      foo(&test3); // Test function pointer foo
      foo(&test4); // Zero args
    }
    

    避免零参数的特殊情况作为重载?

    可以避免零参数 foo 重载,有利于将 last_t 特征调整为也接受空包的特征,以便使用对空包的查询来解析通用重载。然而,它的语义和实现都没有变得如此直接和优雅,因为“空类型列表中的最后一个类型”没有多大意义,这意味着需要将 trait 调整为不同的东西:

    template <typename... Ts> struct last_or_unique_dummy_type {
      using type = typename decltype((std::type_identity<Ts>{}, ...))::type;
    };
    
    template <> class last_or_unique_dummy_type<> {
      struct dummy {};
    
    public:
      using type = dummy;
    };
    
    template <typename... Ts>
    using last_or_unique_dummy_type_t =
        typename last_or_unique_dummy_type<Ts...>::type;
    
    template <typename... ARGS_,
              std::enable_if_t<!std::is_same_v<
                  last_or_unique_dummy_type_t<ARGS_...>, Test *>> * = nullptr>
    void foo(void (*fn)(ARGS_...)) {
      std::cout << "Generic function pointer foo" << std::endl;
    }
    
    template <typename... ARGS_,
              std::enable_if_t<std::is_same_v<last_or_unique_dummy_type_t<ARGS_...>,
                                              Test *>> * = nullptr>
    void foo(void (*fn)(ARGS_...)) {
      std::cout << "Test function pointer foo" << std::endl;
    }
    

    对空包使用额外的重载可能是最不令人惊讶的方法。


    C++20 和 identity_t 技巧

    如果您还没有使用 C++20,那么您自己编写一个身份元函数是微不足道的:

    template <typename T>
    struct type_identity {
      using type = T;
    };
    

    此类模板的任何特化,除非部分/明确地特化(对于 STL 类型是 UB),否则都是微不足道的且可默认构造的。我们在上面last_t 的定义中利用了这一点:在未评估的上下文中默认构造一系列平凡类型,并利用这些类型中的最后一个将输入嵌入到其特化为该平凡类型的身份特征中,并且其包装别名声明type是可变参数包中最后一个参数的类型。

    【讨论】:

    • 我真的很喜欢您使用逗号运算符而不是递归来获取最后一个类型。非常聪明,+1
    • @NathanOliver Afaik 它非常常用于结束参数包转换特征,所以我自己没有想出这个(但我不记得几年前我第一次看到它的地方,因为一个参考),除了现在回想一下我们现在在 STL 中有一个非常适合用途的 std::type_identity(从 C++20 开始)。
    • 就我而言,我只需要手动定义 type_identity,因为我正在使用 C++17。
    【解决方案2】:

    检查ARGS... 的最后一个元素是Test*

    它不会以这种方式为您做到这一点。一种方法是:

    template<class...Ts>
    struct last_type {};
    template<class T1, class T2, class...Ts>
    struct last_type<T1, T2, Ts...>:last_type<T2, Ts...>{};
    template<class T>
    struct last_type<T>{
      using type=T;
    };
    template<class...Ts>
    using last_type_t = typename last_type<Ts...>::type;
    

    现在你只是:

    template <typename... ARGS_>
    requires std::is_same_v<last_type_t<ARGS_...>, Test*>
    void foo(void(*fn)(ARGS_...)) {
       std::cout << "Test function pointer foo" << std::endl;
    }
    

    Live example.

    没有概念,你必须替换那个requires子句:

    template <typename... ARGS_,
      std::enable_if_t<std::is_same_v<last_type_t<ARGS_...>, Test*>, bool> = true
    >
    void foo(void(*fn)(ARGS_...)) {
       std::cout << "Test function pointer foo" << std::endl;
    }
    

    这是一种更模糊的“货物崇拜”方式,基本上是在说同样的事情。 (您还需要在另一个重载中反转启用 if 子句;not = false but !,并处理 0 arg 大小写(在类型列表上添加 void?))

    您的尝试不起作用的原因是 C++ 使 ... 匹配 非常贪婪。在完成参数模式匹配的上下文中,将您想要模式匹配的内容放在其后面通常不是一个好主意。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-23
      • 1970-01-01
      • 2021-10-01
      • 1970-01-01
      • 2019-08-25
      相关资源
      最近更新 更多