首页 > 解决方案 > 用另一个模板重载可变参数模板函数

问题描述

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

#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

解决方案


基于最后一个参数包参数的 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++20std::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为可变参数包中最后一个参数的类型。


推荐阅读