一个例子:对一个序列求和
固定的萃取
用一个函数模板对数组中的所有元素就行求和,我们可以很轻松的写出下面的源码:
#include <iostream>
template <typename T>
T accum(const T *begin, const T *end) {
T total{};
while (begin != end) {
total += *begin;
++begin;
}
return total;
}
int main(int argc, const char **argv) {
int num[] = { 1, 2, 3, 4, 5 };
std::cout << "the average value of the integer values is " << accum(num, num + 5) / 5 << std::endl;
char name[] = "templates";
int length = sizeof(name)-1;
std::cout << "the average value of the characters in \"" << name <<
"\" is " << accum(name, name + 9) / 9 << std::endl;
return 0;
}
然而,程序的运行结果却令人大失所望:
the average value of the integer values is 3
the average value of the characters in "templates" is -5
name所有元素之和为负值,很明显是和超出了char所能表示的范围。所以对数组求和,和的数据类型的表示范围要大于数组的数据类型的表示范围。因此源码中total的类型不能是T。一种解决方案如下:
accum<int>(name,name+5);
另一种解决方案是使用萃取模板,通过模板的偏化建立T和对应返回值类型的联系,如下:
//...
template<typename T> struct AccumulationTraits;
template<>
struct AccumulationTraits<char> { using AccT = int; };
template<>
struct AccumulationTraits<short> { using AccT = int; };
template<>
struct AccumulationTraits<int> { using AccT = long; };
template<>
struct AccumulationTraits<unsigned int> { using AccT = unsigned long; };
template<>
struct AccumulationTraits<float> { using AccT = double; };
template <typename T>
auto accum(const T *begin, const T *end) {
using AccT = typename AccumulationTraits<T>::AccT;
AccT total{};
while (begin != end) {
total += *begin;
++begin;
}
return total;
}
//...
这样返回值类型的问题边解决了,新的执行结果如下:
the average value of the integer values is 3
the average value of the characters in "templates" is 108
值萃取
在accum模板的例子中,我们通过默认构造函数对返回值进行了初始化,但该种方案无法处理本身就没有默认构造函数的类型,如下面的BigInt类:
class BigInt {
public:
BigInt(long long data) : m_data(data) {}
BigInt operator+(const BigInt &rhs) { return BigInt{m_data + rhs.m_data}; }
BigInt &operator+=(const BigInt &rhs) {
m_data += rhs.m_data;
return *this;
}
long long value(void) const { return m_data; }
private:
long long m_data;
};
关于这个问题,我们可以通过值萃取来解决,完整源码如下:
#include <iostream>
class BigInt {
public:
BigInt(long long data) : m_data(data) {}
BigInt operator+(const BigInt &rhs) { return BigInt{m_data + rhs.m_data}; }
BigInt &operator+=(const BigInt &rhs) {
m_data += rhs.m_data;
return *this;
}
long long value(void) const { return m_data; }
private:
long long m_data;
};
template<typename T> struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static const AccT zero = 0; };
template<>
struct AccumulationTraits<short> {
using AccT = int;
static const AccT zero = 0; };
template<>
struct AccumulationTraits<int> {
using AccT = long;
static const AccT zero = 0; };
template<>
struct AccumulationTraits<unsigned int> {
using AccT = unsigned long;
static const AccT zero = 0; };
template<>
struct AccumulationTraits<float> {
using AccT = double;
static constexpr AccT zero = 0.0f; };
template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
static const BigInt zero; };
const BigInt AccumulationTraits<BigInt>::zero = BigInt{0};
template <typename T>
auto accum(const T *begin, const T *end) {
using AccT = typename AccumulationTraits<T>::AccT;
AccT total = AccumulationTraits<T>::zero;
while (begin != end) {
total += *begin;
++begin;
}
return total;
}
如果仔细观察,会发现使用float和BigInt进行偏化与使用整型(char,short,int)进行偏化不的方法不一样:
- c++只允许在类中对整型或枚举类型的static const数据成员进行初始化,所以float进行偏化时使用了float
- 无论是const还是constexpr,都只能对字面值类型(int,float,double)进行初始化,所以使用BigInt进行偏化只能使用上面这种方式。
这样虽然可以工作,但是却有些麻烦(必须在两个地方同时修改代码),这样可能还会有些 低效,因为编译期通常并不知晓在其它文件中的变量定义。为了方便,可以使用函数取代成员变量,如下:
template<typename T> struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static constexpr AccT zero(void) { return 0; } };
template<>
struct AccumulationTraits<short> {
using AccT = int;
static constexpr AccT zero(void) { return 0; } };
template<>
struct AccumulationTraits<int> {
using AccT = long;
static constexpr AccT zero(void) { return 0; } };
template<>
struct AccumulationTraits<unsigned int> {
using AccT = unsigned long;
static constexpr AccT zero(void) { return 0; } };
template<>
struct AccumulationTraits<float> {
using AccT = double;
static constexpr AccT zero(void) { return 0.0f; } };
template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
static BigInt zero(void) { return BigInt{0}; } };
template <typename T>
auto accum(const T *begin, const T *end) {
using AccT = typename AccumulationTraits<T>::AccT;
AccT total = AccumulationTraits<T>::zero();
while (begin != end) {
total += *begin;
++begin;
}
return total;
}
参数化的萃取
前面所提到的萃取方式称为固定萃取。为了更加灵活,我们可以引入一个新的模板参数,专门用于萃取,如下:
template <typename T, typename AT = AccumulationTraits<T>>
auto accum(const T *begin, const T *end) {
using AccT = typename AT::AccT;
AccT total = AccumulationTraits<T>::zero();
while (begin != end) {
total += *begin;
++begin;
}
return total;
}
萃取及策略类
前面的例子没有区分累积和求和,如果想换种累计方式,如求积,该如何处理?最直接了当的方案是把accum拷贝一遍,重新命个名,然后把total += *begin;修改为total *= *begin; ,很明显,这不是我们想要的答案。除了这种直接了当的方案,还可以通过萃取策略来解决这个问题,源码如下:
//...
class SumPolicy {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const& value) { total += value; }
};
class MultPolicy {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const& value) { total *= value; }
};
template<typename T,
typename Policy = SumPolicy,
typename Traits = AccumulationTraits<T>>
auto accum (T const* begin, T const* end) {
using AccT = typename Traits::AccT;
AccT total = Traits::zero();
while (begin != end) {
Policy::accumulate(total, *begin);
++begin;
}
return total;
}
int main(int argc, const char **argv) {
int num[] = { 1, 2, 3, 4, 5 };
std::cout << "the product of the integer values is " << accum<int,MultPolicy>(num, num + 5) << std::endl;
return 0;
}
但程序的运行结果却是0。之所以会这样,是因为我们选取的初始值为0。由此可见,萃取和策略可能会相互影响。通过这个例子,我们有可能会意识到:累计循环的初始值应该累计策略的一部分。 但并不是所有的事情都需要用到萃取和策略才能解决。例如,C++标准库中的 std::accumulate()就将其初始值当作了第三 个参数。
成员模板还是模板模板参数
为实现累积策略,我们将SumPolicy和MultiPolicy实现称为有成员模板的常规类,除此之外,还有另一种方案,使用类模板实现策略接口,如下:
//...
template<typename T1, typename T2>
class SumPolicy {
public:
static void accumulate (T1& total, T2 const& value) { total += value; }
};
template<typename T1, typename T2>
class MultPolicy {
public:
static void accumulate (T1& total, T2 const& value) { total *= value; }
};
template<typename T,
template<typename,typename> class Policy = SumPolicy,
typename Traits = AccumulationTraits<T>>
auto accum (T const* begin, T const* end) {
using AccT = typename Traits::AccT;
AccT total = Traits::zero();
while (begin != end) {
Policy<AccT,T>::accumulate(total, *begin);
++begin;
}
return total;
}
//...
如此实现,优点是策略类的成员变量可以很方便的使用类型信息(如果有);缺点是类必须是模板类,且必须和接口(accum)定义参数一致,这无疑使萃取本身变得更加复杂,也不自然。
类型函数
值函数:接收一些值作为参数,返回一个值作为结果。
类型函数(模板):接收一些类型作为参数,返回一个类型或常量作为结果。以下便是一个类型函数的例子:
template <typename T>
struct type_size {
static const std::size_t value = sizeof(T);
};
int main(int argc, char **argv) {
std::cout << type_size<int>::value << std::endl;
std::cout << type_size<double>::value << std::endl;
return 0;
}
元素类型
通过模板偏特化实现一个工具获取容器中元素的类型,源码如下:
#include <iostream>
#include <vector>
#include <list>
template <typename T>
struct element_t;
template <typename T>
struct element_t<std::vector<T>> {
using type = T;
};
template <typename T>
struct element_t<std::list<T>> {
using type = T;
};
template <typename T, size_t N>
struct element_t<T[N]> {
using type = T;
};
template <typename T>
struct element_t<T[]> {
using type = T;
};
template <typename T>
void print_element_type(const T &container) {
std::cout << typeid(typename element_t<T>::type).name() << std::endl;
}
int main(int argc, char **argv) {
std::vector<std::vector<int>> v1;
print_element_type(v1);
int a1[10];
print_element_type(a1);
return 0;
}
类型函数的作用:允许我们更具容器类型参数化一个模板,但又无需提供代表了元素类型和其他特性的参数,例如:
//需要显示指定元素类型
template<typename T, typename C>
T sumOfElements (C const& c);
//无需显示指定元素类型
template<typename C>
typename ElementT<C>::Type sumOfElements (C const& c);
转换萃取
除了可以被用来访问主参数类型的某些特性,萃取还可以被用来做类型转换,比如为某个类
型添加或移除引用、const 以及 volatile 限制符,下面是移除引用的例子:
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
struct remove_refference_t {
using type = T;
};
template <typename T>
struct remove_refference_t<T &> {
using type = T;
};
template <typename T>
struct remove_refference_t<T &&> {
using type = T;
};
int main(int argc, char **argv) {
std::string str0 = "10";
std::string &str1 = str0;
std::string &&str2 = "0";
std::cout << std::is_same<decltype(str0), decltype(str1)>::value << std::endl;
std::cout << std::is_same<decltype(str0), remove_refference_t<decltype(str1)>::type>::value << std::endl;
std::cout << std::is_same<decltype(str0), decltype(str2)>::value << std::endl;
std::cout << std::is_same<decltype(str0), remove_refference_t<decltype(str2)>::type>::value << std::endl;
return 0;
}
预测性萃取
说到预测性萃取,能想到的第一个便是is_same,用于判断两个类型是否相同,其实现如下:
template <typename T1, typename T2>
struct is_same {
static constexpr bool value = false;
};
template <typename T>
struct is_same<T, T> {
static constexpr bool value = true;
};
其实现也比较好理解,唯一难以理解的就是偏化实现,自己写代码时很少用到这种方式。stl关于is_same的实现如下:
template <typename T, T v>
struct integral_constant {
static const T value= v;
typedef T value_type;
typedef integral_constant type;
};
typedef integral_constant<bool, true> true_type;
typedef integral_constant<bool, false> false_type;
template <typename T1, typename T2>
struct is_same : false_type{} {};
template <typename T>
struct is_same<T, T> : true_type {} {};
与先前的实现相比,新的实现引入了integral_constant,is_same直接从integral_constant派生。如果仔细观察stl,会发现大部分is_xxx函数都是从integral_constant派生。这样做最大的优点就是访问方式统一,对于所有的is_xxx函数都可以通过is_xxx::value访问。
返回结果类型萃取
写一个函数,实现两个向量相加,我们可以很快写出下面的代码:
template <typename T>
std::vector<T> operator+(const std::vector<T> &lhs, const std::vector<T> &rhs) {
size_t min = std::min(lhs.size(), rhs.size());
size_t max = std::max(lhs.size(), rhs.size());
size_t i = 0;
std::vector<T> result;
for (; i < min; ++i)
result.push_back(lhs[i]+rhs[i]);
if (lhs.size() == max) {
while (i < max)
result.push_back(lhs[i++]);
}
if (rhs.size() == max) {
while (i < max)
result.push_back(rhs[i++]);
}
return result;
}
由于C++本身支持一个double类型数值和一个int类型数值求和,因此,我们希望向量也支持这种混合类型的操作。对此,我们很容易想到使用两个模板参数,如下:
template <typename T1, typename T2>
std::vector<???> operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs)
但如何选择返回值的类型,成为了一个难题。面对这个难题,有两个解决方案:
- 使用类型萃取
template <typename T1, typename T2>
struct plus_result { using type = decltype(T1() + T2()); };
template <typename T1, typename T2>
using plus_result_t = typename plus_result<T1, T2>::type;
template <typename T1, typename T2>
std::vector<plus_result_t<T1, T2>> operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs) {
size_t min = std::min(lhs.size(), rhs.size());
size_t max = std::max(lhs.size(), rhs.size());
size_t i = 0;
std::vector<plus_result_t<T1, T2>> result;
for (; i < min; ++i)
result.push_back(lhs[i]+rhs[i]);
if (lhs.size() == max) {
while (i < max)
result.push_back(lhs[i++]);
}
if (rhs.size() == max) {
while (i < max)
result.push_back(rhs[i++]);
}
return result;
}
- 使用auto
template <typename T1, typename T2>
auto operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs) {
size_t min = std::min(lhs.size(), rhs.size());
size_t max = std::max(lhs.size(), rhs.size());
size_t i = 0;
using type = decltype(T1() + T2());
std::vector<type> result;
for (; i < min; ++i)
result.push_back(lhs[i]+rhs[i]);
if (lhs.size() == max) {
while (i < max)
result.push_back(lhs[i++]);
}
if (rhs.size() == max) {
while (i < max)
result.push_back(rhs[i++]);
}
return result;
}
上述两种方案无论哪种,都是使用decltype来推断表达式T1()+T()的类型,因此存在两个共同的问题:
- decltype保留了过多的类型信息,如plus_result::type有可能返回一个引用类型,也可能具有const限制,这可能不是我们想要的,可以通过使用stl标准库工具去掉限制,如下是使用std::remove_reference_t去掉引用的例子
template <typename T1, typename T2>
std::vector<std::remove_reference_t<plus_result_t<T1, T2>>> operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs)
- 由于表达式 T1() + T2()试图对类型 T1 和 T2 的数值进行值初始化,这两个类型必须要有可访问的、未被删除的默认构造函数,如下面的代码是无法通过编译的:
class Int {
public:
Int(int i) : m_Data(i) {}
Int operator+(Int &rhs) { return Int(m_Data + rhs.m_Data); }
public:
int m_Data;
};
class Double {
public:
Double(double d) : m_Data(d) {}
Double operator+(Double &rhs) { return Double(m_Data + rhs.m_Data);}
Double(Int i) : m_Data(i.m_Data) {}
public:
double m_Data;
};
Double operator+(Int lhs, Double rhs) { return Double(lhs.m_Data + rhs.m_Data); }
Double operator+(Double lhs, Int rhs) { return Double(lhs.m_Data + rhs.m_Data); }
//......
std::vector<Int> v7 {{1}, {2}, {3}};
std::vector<Double> v8 {{1.1}, {2.1}, {3.1}};
auto v9 = v7 + v8;
for (auto i : v9)
std::cout << i.m_Data << " ";
std::cout << std::endl;
下面是编译输出的错误之一:编译 decltype(T1() + T2())时,Int找不到合适的构造函数。
result_type_trait3.cpp:51:44: error: no matching constructor for initialization of 'Int'
struct plus_result { using type = decltype(T1() + T2()); };
解决方案就是使用std::declval重写plus_result,如下:
template <typename T1, typename T2>
struct plus_result { using type = decltype(std::declval<T1>() + std::declval<T2>()); };
有了std::declval,我们就可以在使用decltype推断类型时,不依赖默认构造函数。