这篇博客记录一下学习 C++ 模板中遇到的一些知识,参考书籍是 C++ Templates: The Complete Guide (2nd Edition),本文所用的编译器版本:
> g++ --version
g++ (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
目录
typename
的意义
在模板声明中,typename
和 class
没有任何区别,但是在一个模板类里,如果要声明一个模板类中的一个类型的变量,例如 vector<T>::iterator
,也就是模板依赖,则需要在最前面加上关键字 typename
来告诉编译器这是一个类型,而不是模板类中的静态成员或者常量:
template<class T>
class A {
typename T::some_type* variable;
};
如果写成:
template<class T>
class A {
T::some_type* variable;
};
编译器就会将 T::some_type* variable
当做两个成员相乘。
如果将
template <class T>
void printContainer(const T& container) {
typename T::const_iterator it, end(container.cend());
for (it = container.cbegin(); it != end; ++it)
cout << *it << endl;
}
写成
template <class T>
void printContainer(const T& container) {
T::const_iterator it, end(container.cend());
for (it = container.cbegin(); it != end; ++it)
cout << *it << endl;
}
编译器会报错:
main.cpp: In function ‘void printContainer(const T&)’:
main.cpp:20:11: error: need ‘typename’ before ‘T::const_iterator’ because ‘T’ is a dependent scope
const T::const_iterator it, end(container.cend());
变长模板参数
#include <iostream>
#include <vector>
using namespace std;
void print() {}
template <class Arg, class... Rest>
void print(Arg first, Rest... rest) {
cout << first << endl;
print(rest...);
}
template <class Collection, class... Indicies>
void printElems(const Collection& coll, Indicies... indicies) {
print(coll[indicies]...);
}
int main(int argc, char const *argv[]) {
vector<string> strings{"first", "second", "third", "fourth", "fifth"};
print(1, 2, 3, 4, 5);
printElems(strings, 3, 1, 0);
return 0;
}
默认初始化成员值
template<typename T>
void foo() {
T x{};
}
模板模板参数
假如现在有一个类:
template<class ElementType, class Container>
class Stack {
Container container;
// ...
};
那么调用的时候就要像这样使用:
Stack<int, std::deque<int>> stack;
如果想这样使用:
Stack<int, std::deque> stack;
就要像这样声明模板:
template<class ElementType, template <class T> class Container = std::deque>
class Stack {
Container<ElementType> container;
// ...
};
这么做还不行,因为 STL 中的容器类实际上有两个模板参数,除了元素类型,还有 allocator
类型,在默认匹配的时候,如果只提供一个参数,编译器无法匹配,所以我们要写成下面这种形式:
template<class ElementType,
template <class T, class Alloc = std::allocator<T>> class Container = std::deque>
class Stack {
Container<ElementType> container;
// ...
};
移动语义
在 C++ 11 中新增了移动语义,可以减少不必要的拷贝操作,但是在模板中,如果这样写:
class X {
//...
};
void g (X&) {
std::cout << "g() for variable\n";
}
void g (X const&) {
std::cout << "g() for constant\n";
}
void g (X&&) {
std::cout << "g() for movable object\n";
}
template <typename T>
void f(T&& val) {
g(val);
}
int main()
{
X v; // create variable
X const c; // create constant
f(v); // f() for variable calls f(X&) => calls g(X&)
f(c); // f() for constant calls f(X const&) => calls g(X const&)
f(X()); // f() for temporary calls f(X&&) => calls g(X&&)
f(std::move(v)); // f() for move-enabled variable calls f(X&&) => calls g(X&&)
}
得到的输出是:
g() for variable
g() for constant
g() for variable
g() for variable
需要用 std::forward<>
来完美转发 (Perfect Forwarding)潜在的移动语义:
template<typename T>
void f (T&& val) {
g(std::forward<T>(val)); // call the right g() for any passed argument val
}
输出是:
g() for variable
g() for constant
g() for movable object
g() for movable object
enable_if<>
的应用
为了利用右值引用和移动语义,在声明类和构造函数的时候可以分别为左值和右值写构造函数:
#include <iostream>
#include <string>
#include <utility>
class Person {
private:
std::string name;
public:
// constructor for passed initial name:
explicit Person(std::string const& n) : name(n) {
std::cout << "copying string-CONSTR for '" << name << "'\n";
}
explicit Person(std::string&& n) : name(std::move(n)) {
std::cout << "moving string-CONSTR for '" << name << "'\n";
}
// copy and move constructor:
Person(Person const& p) : name(p.name) {
std::cout << "COPY-CONSTR Person '" << name << "'\n";
}
Person(Person&& p) : name(std::move(p.name)) {
std::cout << "MOVE-CONSTR Person '" << name << "'\n";
}
};
int main() {
std::string s = "sname";
Person p1(s); // init with string object => calls copying string-CONSTR
Person p2("tmp"); // init with string literal => calls moving string-CONSTR
Person p3(p1); // copy Person => calls COPY-CONSTR
Person p4(std::move(p1)); // move Person => calls MOVE-CONST
}
利用上面的转发机制,可以将构造函数写成模板函数:
class Person {
private:
std::string name;
public:
// generic constructor for passed initial name:
template <typename STR>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {
std::cout << "TMPL-CONSTR for '" << name << "'\n";
}
// copy and move constructor:
Person(Person const& p) : name(p.name) {
std::cout << "COPY-CONSTR Person '" << name << "'\n";
}
Person(Person&& p) : name(std::move(p.name)) {
std::cout << "MOVE-CONSTR Person '" << name << "'\n";
}
};
但是这么做会有问题,在编译的时候,下面的语句会报错:
Person p3(p1); // copy Person => calls COPY-CONSTR
原因是,对于左值 p1
来说,调用模板函数只需要简单地将类型 STR
替换为 Person&
,但是对于拷贝构造函数而言,则需要一次额外的 const
转换,因此解决函数重载的时候,模板函数比下面的拷贝构造函数更优。
一种可能的解决办法是像下面这样定义一个构造函数:
Person(Person& p)
然而对于 Person
的派生类而言,模板函数依然是更优的匹配,不能完全解决问题。我们需要的解决方案是,对于从 Person
或者其派生类的对象,需要屏蔽掉模板函数。C++ 11 中提供了 std::enable_if<>
来解决这个问题。
enable_if<>
的作用
template<typename T>
typename std::enable_if<condition, OptionalType>::type
foo() {
// ...
}
- 当
condition
为真的时候,type
类型就是OptionalType
,如果第二个模板参数没有指定,那么就是void
- 当
condition
为假的时候,type
类型未定义,由 SFINAE(Substitution Failure Is Not An Error) 原则,该模板当前的特化被忽略,达到屏蔽的效果
一般也可以写成下面这种形式,更简洁一些:
template<typename T, typename = std::enable_if<condition>::type>
void foo() {
// ...
}
回到上面 Person
类的问题,我们可以通过这样的方式来在特定时候启用模板构造函数:
#include <type_traits>
template<typename T>
using EnableIfString = std::enable_if<
std::is_convertible<T,std::string>::value>::type;
// ...
template<typename STR, typename = EnableIfString<STR>>
explicit Person(STR&& n)
: name(std::forward<STR>(n)) {
std::cout << "TMPL-CONSTR for '" << name << "'\n";
}
// ...
就可以解决问题了。
连接问题
按照惯常的做法,一般我们会在写大程序的时候,将声明放在头文件中,比如 .h
一类的,然后在另外的 .cpp
文件中定义函数。但是如果按照这种方式组织模板代码,会发生下面的问题:
// myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template<typename T>
void printTypeof (T const&);
#endif // MYFIRST_HPP
// myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x)
{
std::cout << typeid(x).name() << '\n';
}
// myfirstmain.cpp
#include "myfirst.hpp"
// use of the template
int main()
{
double ice = 3.0;
printTypeof(ice); // call function template for type double
}
当我们试着编译这段程序的时候,会报连接错误:
> g++ myfirstmain.cpp myfirst.cpp -o myfirst
/tmp/cc3q27zY.o: In function `main':
myfirstmain.cpp:(.text+0x2c): undefined reference to `void printTypeof<double>(double const&)'
collect2: error: ld returned 1 exit status
原因是编译器在遇到模板函数的时候需要实例化,但是实例化所需要的信息分别在两个编译模块中,因此,当编译器遇到 printTypeof()
调用的时候,并不知道如何具体实例化这个函数,只能假定在别的地方定义了实例函数;而当编译器遇到了 printTypeof()
的模板定义的时候,它并不知道需要用什么模板参数去实例化,也就没有实例化,因此导致了连接错误。
解决办法是将模板函数的定义放在头文件中:
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
#include <iostream>
#include <typeinfo>
// declaration of template
template<typename T>
void printTypeof (T const&);
// implementation/definition of template
template<typename T>
void printTypeof (T const& x)
{
std::cout << typeid(x).name() << '\n';
}
#endif // MYFIRST_HPP
这样可以解决问题,但是会增加编译时间。另一种办法是模板显式实例化:
// myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x)
{
std::cout << typeid(x).name() << '\n';
}
template void printTypeof<>(double const&);
但是这样就需要每种可能的模板参数都声明一遍,所以还是内含在头文件中比较方便,尽管会带来额外的编译开销。
类型特性(Type Traits)
从 C++ 11 起,可以通过引入头文件 <type_traits>
实现编译时的类型特性检查和操作,在编写通用库的时候非常有用。
其中值得注意的是 std::remove_const_t<>
和 std::remove_reference_t<>
两者的使用顺序:
std::remove_const_t<int const&> // -> int const&
std::remove_const_t<std::remove_reference_t<int const&>> // -> int
std::remove_reference_t<std::remove_const_t<int const&>> // -> int const
当然还可以:
std::decay_t<int const&> // -> int
特性(Traits)
假设有一个求和函数:
template<typename T>
T accum (T const* beg, T const* end)
{
T total{}; // assume this actually creates a zero value
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
这段模板代码在如下调用的时候会发生溢出错误:
char name[] = "templates";
int length = sizeof(name)-1;
// (try to) print average character value
std::cout << "the average value of the characters in \""
<< name << "\" is "
<< accum(name, name+length) / length
<< '\n';
原因是,模板中的类型 T
被实例化成为 char
,所以发生了溢出错误,当然我们可以通过额外声明求和变量的类型来解决问题:
accum<int>(name, name+length)
类型特性(Type Traits)
通过类型特性,可以更好地解决这个问题:
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
};
// ...
这里的技巧是通过模板特化来选择我们需要的类型,然后求和函数代码只需要改造成下面这样既可:
template<typename T>
auto accum (T const* beg, T const* end)
{
// return type is traits of the element type
using AccT = typename AccumulationTraits<T>::AccT;
AccT total{}; // assume this actually creates a zero value
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
值特性(Value Traits)
在原本的代码中,AccT
是根据一个 T
推断出来的类型,并且使用了花括号进行默认构造,但是 AccT
很可能没有默认构造函数,或者不能提供一个足够好的默认值,这时候可以用值特性来提供默认值:
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static AccT const zero = 0;
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
static AccT const zero = 0;
};
模板代码改造成下面这样:
template<typename T>
auto accum (T const* beg, T const* end)
{
// return type is traits of the element type
using AccT = typename AccumulationTraits<T>::AccT;
AccT total = AccumulationTraits<T>::zero; // init total by trait value
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
但是这样也有限制:C++ 在类中的静态常量只能使用整形或者字面量来初始化。
SFINAE
通过SFINAE,可以实现std::is_default_constructible<>
的功能,首先利用值特性创建布尔常量:
template<bool val>
struct BoolConstant {
using Type = BoolConstant<val>;
static constexpr bool value = val;
};
using TrueType = BoolConstant<true>;
using FalseType = BoolConstant<false>;
通过模板特化,实现判断两个类型是否相同的功能:
#include "boolconstant.hpp"
template <typename T1, typename T2>
struct IsSameT : FalseType {};
template <typename T>
struct IsSameT<T, T> : TrueType {};
最后实现模板来检查一个类是否有默认构造函数:
#include "issame.hpp"
template<typename T>
struct IsDefaultConstructibleT {
private:
// test() trying substitute call of a default constructor for T passed as U:
template<typename U, typename = decltype(U())>
static char test(void*);
// test() fallback:
template<typename>
static long test(...);
public:
static constexpr bool value
= IsSameT<decltype(test<T>(nullptr)), char>::value;
};
这段代码的意思是:尝试用模板类中的 T
替换模板类中的模板函数中的 U
,假如没有默认构造函数,那么 decltype(U())
就会发生错误,从而 char test(void*)
替换失败,这时候会回退到下面定义的函数,由于参数列表是省略号,因此总是最后匹配到,这时候 test()
的返回值类型就变为 long
。利用这个特性,我们只需要判断 test()
的返回值类型就知道有没有默认构造函数了。
注意,如果我们直接用 T
来测试,而不是通过传递模板参数的方法的话,会直接产生编译错误:
template<typename, typename = decltype(T())>
static char test(void*);
这是因为在模板实例化的时候,所有的成员函数都会被替换进去,如果没有默认构造函数,就会发生编译错误,并且不会触发 SFINAE
。通过传递模板参数的方法,则创造了一个局部性的模板替换上下文,从而可以触发 SFINAE
。
改进版本
当然这么写有点复杂,我们可以直接让函数的返回类型就是布尔常量,同时也符合了一个谓词模板应该返回布尔常量类的惯例:
#include <type_traits>
template<typename T>
struct IsDefaultConstructibleHelper {
private:
// test() trying substitute call of a default constructor for T passed as U:
template<typename U, typename = decltype(U())>
static std::true_type test(void*);
// test() fallback:
template<typename>
static std::false_type test(...);
public:
using Type = decltype(test<T>(nullptr));
};
template<typename T>
struct IsDefaultConstructibleT : IsDefaultConstructibleHelper<T>::Type {
};
利用偏特化改进
#include <type_traits> // defines true_type and false_type
// helper to ignore any number of template parameters:
template<typename...> using VoidT = void;
// primary template:
template<typename, typename = VoidT<>>
struct IsDefaultConstructibleT : std::false_type
{
};
// partial specialization (may be SFINAE'd away):
template<typename T>
struct IsDefaultConstructibleT<T, VoidT<decltype(T())>> : std::true_type
{
};