从typetraits到concept,一个简单介绍

发布时间 2023-05-29 00:26:04作者: xiaomingcc

什么是typetraits?

以标准库中的std::is_integral<T>为例,它可以用于判断其类型参数T是否为一个整数类型,我们可以编写如下的示例代码:

template<typename T>
void print(const T& t){
  if(std::is_integral<T>::value){
    std::cout << "A integral value: " << t << std::endl;
  }else{
    std::cout << "A non-integral value: " << t << std::endl;
  }
}

上面的模板函数代码很简单,就是在传入参数为整数类型时与非整数类型时分别打印不同的提示语以及该参数。
这个示例展示了typetraits最主要的功能:在template编程中,对于不同的类型提供不同实现或者行为,而不需要显式地列出所有的重载版本。 对于以上的例子而言,如果不使用std::is_integral,那么也可以通过编写若干个接收参数类型分别为boolcharint等等整数类型的重载版本,再加上一个针对非整数类型的模板函数,也可以实现相同的功能。
此外,由于std::is_integral<T>::value是一个编译期间常量,这意味着我们可以使用if constexpr(std::is_integral<T>::value) 来使得该分支在编译期间被确定,从而使得运行时没有额外的判断开销。(if constexpr是C++17中引入的)。

typetraits是如何实现的?

首先,像std::is_integral<T>这样的typetraits实际上是一个模板类(template struct),而value是它的一个static成员值。如果想要实现一个自己的is_integral,可以借助标准库提供的std::false_typestd::true_type

template <typename T> struct my_integral_helper : public std::false_type{};
template <> struct my_integral_helper<int> : public std::true_type{};
template <> struct my_integral_helper<bool> : public std::true_type{};
template <> struct my_integral_helper<char> : public std::true_type{};
// more integral types
...
// my_integral<T>
template <typename T> my_integral : public my_integral_helper<typename std::remove_cv<T>::type>{};
// my_integral_v<T>  
template <typename T> constexpr bool my_integral_v  = my_integral<T>::value;

在上述代码中,我们定义了一个模板struct my_integral_helper,它继承了std::false_type。而对于每一个我们所需要的整数类型T,都需要为其定义一个特化的继承了std::true_typemy_integral_helper。这样,借助模板的特化,我们就可以在编译期间确定一个类型是否满足要求。
进一步,借助std::remove_cv还可以去除掉类型上的const/volatile qualifier。这样就可以定义出一个通用版本的my_integral<T>类型,它的功能与标准库的std::is_integral<T>是一样的。实际上,在标准库中的对std::is_integral实现跟上述的my_integral的实现几乎没有什么区别。

concepts

在某种程度上,可以使用C++20中引入的concepts来完成类似的功能,还是以上面的print函数为例:

template <typename T>
void print(const T& t){
  std::cout << "A non-integral value: " << t << std::endl;
}
template <typename T>
requires std::is_integral<T>::value
void print(const T& t){
	std::cout <<"A integral value: " << t << std::endl;
}

上面这段代码借助requires关键字,要求第二个函数模板中的类型T必须满足std::is_integral<T>::value为true的条件。从而可以实现与之前typetraits版本的print函数一样的功能。
如果需要了解concepts,那么首先需要了解requires关键字。
requires表达式的基本语法如下所示:

requires { requirement-seq }
requires (parameter-list)

通过requires语句,我们可以定义一个concept,如下面的例子:

template <typename T>
concept Addable = requires (T t){
  { t + t } -> std::convertible_to<T>;
};

这个例子定义了一个名为Addalbeconcept,它要求类型T的对象t满足 t+t可以求值且结果类型可以转换为T类型。借助这个concept,我们可以定义一个函数Add:

template <typename T>
requires Addable<T>
T Add(const T& a, const T& b){
   return  a + b;
}

此外,也可以定义多个concept并用组合的requires语句:

template <typename T>
concept Subtractable = requires(T t){
  {t - t} -> std::convertible_to<T>;
};
template <typename T>
concept Arithmetic = requires(T t){
  { t + t} -> std::convertible_to<T>;
  { t - t }-> std::convertible_to<T>;
};

或者也可以直接组合它们:

template <typename T>
concept Arithmetic = Addable<T> && Subtractable<T>;
// 这里不需要requires语句,可以直接使用布尔运算符组合多个不同的concept

使用concept来约束模板中所使用的类型参数T,一方面是在使用模板时可以得到更加准确的约束并且能够得到更加友好的提示信息。另一方面则是可以通过concept实现更加方便高效的模板函数重载。