特性、策略与标签
利用迭代器,我们可以实现很多通用算法,迭代器在容器与算法之间搭建了一座桥梁。适用所有类型的求和函数模板如下:
#include <iostream>
#include <vector>
template<typename iter> //要求我们输入的模板类型iter是个迭代器类型
//用迭代器实现所有类型的求和
typename iter::value_type mysum(iter begin, iter end)
{
typename iter::value_type sum(0);
for(iter i=begin; i!=end; ++i)
sum += *i;
return sum;
}
int main()
{
std::vector<int> v;
for(int i = 0; i<100; ++i)
v.push_back(i);v.push_back(i);
std::cout << mysum(v.begin(), v.end()) << '\n';
}
程序编译输出:4950。
我们想让 mysum() 对指针参数也能工作,毕竟迭代器就是模拟指针,但指针没有嵌套类型 value_type,可以定义 mysum() 对指针类型的特例,但更好的办法是在函数参数和 value_type 之间多加一层特性(traits)。
template<typename iter>
class mytraits //标准容器通过这里获取容器元素的类型
{
public: typedef typename iter::value_type value_type;
};
template<typename T>
// mytraits的指针偏特化版本
class mytraits<T*> //数组类型的容器,通过这里获取数组元素的类型
{
public: typedef T value_type;
};
template<typename iter>//iter可以是个迭代器 也可以是个指针类型
typename mytraits<iter>::value_type mysum(iter begin, iter end)
{
typename mytraits<iter>::value_type sum(0);//如果iter是指针类型,则其中value_type就是iter去掉*后的类型
for(iter i=begin; i!=end; ++i)
sum += *i;
return sum;
}
int main()
{
int v[4] = {1,2,3,4};
//传入v,自动推导出iter的类型为int*,因为是iter是指针类型,所以调用mytraits的指针特化版本在这个特化版本中 value_type被定义为int类型
std::cout << mysum(v, v+4) << '\n';
return 0;
}
程序输出:10
其实,C++ 标准定义了类似的 traits (可同时适用于指针和迭代器的遍历), std::iterator_trait(另一个经典例子是 std::numeric_limits) 。
traits特性对类型的信息(如 value_type、 reference)进行包装,使得上层代码可以以统一的接口访问这些信息。
C++ 模板元编程会涉及大量的类型计算,很多时候要提取类型的信息(typedef、 常量值等),如果这些类型信息的访问方式不一致(如上面的迭代器和指针),我们将不得不定义特例,这会导致大量重复代码的出现(另一种代码膨胀),而通过加一层特性(如上面的mytraits统一了接口)可以很好的解决这一问题。
另外,特性不仅可以对类型的信息进行包装,还可以提供更多信息,当然,因为加了一层,也带来复杂性。特性是一种提供元信息的手段。
策略(policy)一般是一个类模板,典型的策略是 STL 容器的分配器(如std::vector<>,完整声明是template
标签(tag)一般是一个空类,其作用是作为一个独一无二的类型名字用于标记一些东西,典型的例子是 STL 迭代器的五种类型的名字。
input_iterator_tag
output_iterator_tag
forward_iterator_tag
bidirectional_iterator_tag
random_access_iterator_tag (随机访问迭代器tag)
实际上,std::vector
#include <iostream>
#include <vector>
#include <type_traits>
int main()
{
std::cout << is_same<std::vector<int>::iterator::iterator_category, std::random_access_iterator_tag >::value << std::endl;
return 0;
}
程序输出:1。
有了这样的判断,还可以根据判断结果做更复杂的元编程逻辑(如一个算法以迭代器为参数,根据迭代器标签进行特例化以对某种迭代器特殊处理)。标签还可以用来分辨函数重载。
整型数值类型
template <class T, T v>
struct integral_constant {
static const T value = v;
typedef T value_type;
typedef integral_constant type;
};
integral_constant 模板同时包含了整数的类型和数值,而通过这个类型的 value 成员我们又可以重新取回这个数值。有了这个模板的帮忙,我们就可以进行一些更通用的计算了
上面讲的内容都没有离开c++98,下面讲在现代c++中的做法
编译期类型推导
C++ 标准库在
1 true_type or false_type
为了方便地在值和类型之间转换,标准库定义了一些经常需要用到的工具类。上面描述的 integral_constant 就是其中一个(上面写的定义实际有所简化)。为了方便使用,针对布尔值有两个额外的类型定义
typedef std::integral_constant<bool, true> true_type;
typedef std::integral_constant<bool, false> false_type;
//本质上是结构体
这两个标准类型 true_type 和 false_type 经常可以在函数重载中见到。有一个工具函数常常会写成下面这个样子
template <typename T>
class SomeContainer {
public:
…
//很多容器里会有一个destory函数,通过指针来析构某个对象
static void destroy(T* ptr)
{
_destroy(ptr,
is_trivially_destructible<
T>());// 两个重载版本在下面 destroy(T* ptr, true_type or false_type)
//is_trivially_destructible 模板来用于判断类是否是可平凡析构的——也就是说,
//不调用析构函数,不会造成任何资源泄漏问题(即判断此类是否有管理堆上资源)。
//--- 其实只要显示定义或重载了析构函数 那么那个类T就无法平凡析构,说着T类中有非POD成员(即类成员)则也无法平凡析构
}
private:
static void _destroy(T* ptr,
true_type)
{}// 类可以平凡析构 即T类中没有重新定义类的析构函数 即只有默认析构函数 那么可以什么也不做 因为默认析构本身就是什么也不做
static void _destroy(T* ptr,
false_type)
{
ptr->~T();/ 类不可以平凡析构 即T类中重新定义类的析构函数 重写了析构 那么我们必须调用T的析构函数来释放资源
}
//这两个重载根据形参的类型不同来区分,但都没有为形参取名(仅仅用于区分重载的形参 在函数中并没有用到 所以可以不取形参名)。
};
is_trivially_destructible
像 is_trivially_destructible 这样的 trait 类有很多,可以用来在模板里决定所需的特殊行为:is_array,is_enum,is_function,is_pointer,is_reference,is_const,has_virtual_destructor
都是返回调用这些模板的实例化等同于 true_type or false_type
这些特殊行为判断可以是像上面这样用于决定不同的重载(调用构造的方式得到true_type or false_type),也可以是直接用在模板参数甚至代码里(记得我们是可以直接得到布尔值的 取::value)。
2 remove_const
除了得到布尔值和相对应的类型的 trait 模板,我们还有另外一些模板,可以用来做一些类型的转换。以一个常见的模板 remove_const 为例(用来去除类型里的 const 修饰),它的定义大致如下:
template <class T>
struct remove_const {
typedef T type;//通用版本 如果remove_const<常类型外的其他类型>调用这个版本,可以看到下面type就是T 因为T本身就是不带const的 符合我们给予它的行为
};
template <class T>
struct remove_const<const T> {// 常类型特化版本 如果remove_const<一个常类型>会调用这个特化版本 可以看到下面type为T 是去掉cosnt的类型
typedef T type;
};
remove_const<const string>::type 等价于 string
细节! 对于指针的 顶层(常指针 指向不可改) 和 底层 const(常数指针 内容不可改)。对顶层指针remove_const是可以去掉顶层cosnt的,因为本质是指针 去常。
但是对底层const是去不掉的,原因是,const char 是指向 const char 的指针,而不是指向 char 的 const 指针。本质是因为 指针不是常的
const string&应用remove_const后还是const string&(只有当同时出现指针概念和const才会出现顶层和底层const的区别 因为而引用本质是顶层const const string&中的const是修饰string的是底层const 所以这里无法去掉修饰string的这个底层const),const string(只有一个cosnt 都视为顶层const 可以直接去掉)应用remove_const后是string。
对::取内容的简化
如果你觉得写 *is_trivially_destructible
template <class T>
inline constexpr bool
is_trivially_destructible_v = //增加_v的类型别名 不需要再写::value, 而是直接写_v取value
is_trivially_destructible<
T>::value;
//is_trivially_destructible<T>::value == is_trivially_destructible_v<T>
template <class T>
using remove_const_t =
typename remove_const<T>::type; //增加_t的类型别名 不需要再写::type, 而是直接写_t取type
//remove_const<T>::type == remove_const_t<T>
using 是现代 C++ 的新语法,功能大致与 typedef 相似,但 typedef 只能针对某个特定的类型,而 using 可以生成别名模板。目前我们只需要知道,在你需要 trait 模板的结果数值和类型时,使用带 _v 和 _t 后缀的模板可能会更方便,尤其是带 _t 后缀的类型转换模板。
const T& 等同于 T const&,但和 T& const 不同。前者是一个指向常量的引用(T const&==const T&这里明确写出的const为底层const,引用的那个隐式cosnt为顶层const),后者是一个常引用(T& const 引用的那个隐式cosnt为顶层const,这里加上的const没用—-不存在所谓的常引用,引用一定是顶层cosnt的,引用的类型是不能改的)。只有后者才被看作是一个“常量”。
例子通用的fmap函数模板
下面我们演示一个 map 函数(当然,在 C++ 里它的名字就不能叫 map 了,这个map实际指的是映射—对传入函数的容器内容做统一的函数映射),其中用到了目前为止我们学到的多个知识点:
template <
template <typename, typename>
class OutContainer = vector,//缺省使用 vector 作为返回值的容器,但可以通过模板参数改为其他容器
typename F, class R>
auto fmap(F&& f, R&& inputs)
{
typedef decay_t<decltype(
f(*inputs.begin()))>
//用 decltype 来获得用 f 来调用 inputs 元素的类型,调用一次我们要做的函数映射f(*inputs.begin()) 并通过decltype取得函数映射返回值的类型
//用 decay_t (_t后缀)来把获得的类型变成一个普通的值类型
result_type;
OutContainer<
result_type,
allocator<result_type>>
result;//输出的结果容器
//使用基于范围的 for 循环来遍历 inputs,对其类型不作其他要求
for (auto&& item : inputs) {
result.push_back(f(item));//存放结果的容器需要支持 push_back 成员函数
//对万能引用使用完美转发是否好点,
//result.push_back(std::forward<decltype(f(item))>(f(item)))或//result.emplace_back(std::forward<decltype(f(item))>(f(item)));;
}
return result;//自动推导result 类型
}
vector<int> v{1, 2, 3, 4, 5};
int add_1(int x)
{
return x + 1;
}
auto result = fmap(add_1, v);
在 fmap 执行之后,我们会在 result 里得到一个新容器,其内容是 2, 3, 4, 5, 6。
一个多重模板的例子
template <int num1, int num2>
struct Add_
{
const static int res = num1 + num2;
};
template <int num1, int num2>
struct Sub_
{
const static int res = num1 - num2;
};
template <bool Condition>
struct If_;
template <>
struct If_ <true>
{
template<int num1, int num2>
using type = Add_<num1, num2>;
};
template <>
struct If_ <false>
{
template<int num1, int num2>
using type = Sub_<num1, num2>;//相当于两个参数都给定的全特化
};
template<int num1, int num2>
template<bool Condition>
using If = typename If_<Condition>::template type<num1, num2>;
template<int num1, int num2>
using True = If<true>;
template<int num1, int num2>
using False = If<false>;
{ // 更好的写法
template <bool Condition, int num1, int num2>
using If = typename If_<Condition>::
template type<num1, num2>;
template <int num1, int num2>
using True = If<true, num1, num2>;
template <int num1, int num2>
using False = If<false, num1, num2>;
}