C++ 模板函数以随机顺序接受参数

问题描述

我正在编写一个 C++ 网络库,并希望主(模板)函数随机顺序接受参数,使其更加用户友好,就像 CPR图书馆可以。

模板函数最多可同时接受 10 个参数,每个参数类型不同。有没有办法实例化模板以接受参数类型的任何随机顺序,而不是必须手动包含每种可能性的代码

例如 - 在这种情况下使用 3 个参数,每个参数的类型不同:

.h 文件

namespace foo
{
    template <typename T,typename U,typename V> void do(const T& param_a,const U& param_b,const V& param_c);
};

.cpp 文件

template <typename T,typename V>
void foo::do(const T& param_a,const V& param_c) {
//do lots of stuff
}

//instantiate to allow random param order 
template void foo::do<int,std::string,long>(const int&,const std::string&,const long&);
template void foo::do<int,long,std::string>(const int&,const long&,const std::string&);
template void foo::do<int,int>(const int&,const int&);
//etc... to cover all possible param orders

解决方法

如果您的目标是匹配给定库的 API 设计,最好的学习方法是深入研究其源代码并对其进行剖析。

考虑这段代码(我仍然使用 CPR 作为示例,因为您提到它作为参考):

cpr::Session session;
session.SetOption(option1);
session.SetOption(option2);
session.SetOption(option3);

您需要一个可以处理 option1,option2,... 的方法,无论它们以何种顺序提供。对 SetOption 的后续调用可以替换为单个 SetOptions(option3,option1,option2)。因此我们需要一个可变参数 SetOptions 方法:

template<typename Ts...> // important: don't specialize the possible argument types here
void SetOptions(Ts&&... ts)
{ /* do something for each param in ts... */ }

问题是“您如何为 SetOption 参数包中的每个项目调用 ts ?”。这是std::initializer_list的使命。您可以找到一个简单的示例 here

这里的关键是有一个重载函数,它可以单独处理每个参数类型(example 在 CPR 中使用 SetOptions)。然后,在您的“可变”函数中,您为每个参数调用重载函数,一次一个(CPR 中的 example,然后在 various places).

需要注意的一点是,您可以传递多个相同类型的参数。根据您想要实现的目标,这可能是一个问题,也可能不是。

此外,您可以使用不受支持的参数类型(不匹配任何重载)调用该方法,在这种情况下,错误消息并不总是明确的,具体取决于您使用的编译器。然而,这是您可以使用 static_assert 克服的问题。

,

有没有办法实例化模板以接受参数类型的任何随机顺序,而不是必须手动包含每种可能性的代码?

对于没有宏的显式实例化定义,您不能这样做,但是您可以使用单独的方法并依赖隐式实例化,使用 SFINAE 基于两个自定义特征来限制主模板(您将其定义移至头文件) .

首先,给定以下类型序列

template <class... Ts>
struct seq {};

我们想要构造一个特征,对于给定的类型序列 seq<T1,T2,...>(您的“10 个参数类型”),表示为 s

  • s 应是您选择的一组类型的子集 seq<AllowedType1,...>,并且
  • s 应仅包含唯一类型。

我们可以将前者实现为:

#include <type_traits>

template <class T,typename... Others>
constexpr bool is_same_as_any_v{(std::is_same_v<T,Others> || ...)};

template <typename,typename> struct is_subset_of;

template <typename... Ts,typename... Us>
struct is_subset_of<seq<Ts...>,seq<Us...>> {
  static constexpr bool value{(is_same_as_any_v<Ts,Us...> && ...)};
};

template <typename T,typename U>
constexpr bool is_subset_of_v{is_subset_of<T,U>::value};

后者为

template <typename...> struct args_are_unique;

template <typename T> struct args_are_unique<T> {
  static constexpr bool value{true};
};

template <typename T,typename... Ts> struct args_are_unique<seq<T,Ts...>> {
  static constexpr bool value{!is_same_as_any_v<T,Ts...> &&
                              args_are_unique<seq<Ts...>>::value};
};

template <typename... Ts>
constexpr bool args_are_unique_v{args_are_unique<Ts...>::value};

之后我们可以将主模板定义为

namespace foo {
namespace detail {
using MyAllowedTypeSeq = seq<int,long,std::string>;  // ...
} // namespace detail

template <
    typename T,typename U,typename V,typename Seq = seq<T,U,V>,typename = std::enable_if_t<is_subset_of_v<Seq,detail::MyAllowedTypeSeq> &&
                                args_are_unique_v<Seq>>>
void doStuff(const T &param_a,const U &param_b,const V &param_c) {
  // do lots of stuff
}
} // namespace foo

以及我们可以和不可以使用主模板重载的地方,如下所示:

int main() {
  std::string s{"foo"};
  int i{42};
  long l{84};
  foo::doStuff(s,i,l); // OK
  foo::doStuff(s,l,i); // OK
  foo::doStuff(l,s); // OK
  foo::doStuff(l,s,i); // OK

  // uniqueness
  foo::doStuff(l,i); // Error: candidate template ignored

  // wrong type
  unsigned int ui{13};
  foo::doStuff(s,ui,l); // Error: candidate template ignored
}

如果类型实际上不需要是唯一的(从问题中有点不清楚),您可以简单地 SFINAE-仅在第一个 is_subset_of_v 特征上约束主模板:

template <
    typename T,detail::MyAllowedTypeSeq>>>
void do(const T &param_a,const V &param_c) {
  // do lots of stuff
}
,

为什么不在这里使用构建器模式?您将使用各种 foo_builder 方法和最终的 setXxx 创建一个 build() 以获取完全配置的对象。

,

使用结构体保存所有参数。

namespace foo
{
    struct do_params {
        int a;
        long b;
        std::string c;
    };
    void do(do_params params);
};