Tricky Basics of C++ Templates
这章将介绍关于模板的一些看起来有点棘手或者诡异的知识点,比如 typename 的其他用途、零初始化、函数模板的字符串字面值参数等等。这些知识点在你常年的模板实践中总会遇到。
关键字 typename
在模板内部,可以使用关键字 typename 指明一个标识符为一个类型。例如:
template<typename T>
class MyClass {
public:
...
void foo() {
typename T::SubType* ptr;
}
};
使用 typename 告诉编译器 SubType 是类内定义的一个类型。没有关键字 typename 前缀的话,编译器会将 SubType 当作一个没有类型的数据成员,而 T::SubType* ptr 会被认为是一个乘法表达式。
零初始化
对于作用域内的内置类型(int,double,pointer,etc.),没有显示进行初始化赋值时,将会产生一个未定义的值。
void foo()
{
int x;
int* ptr;
}
对于模板,如果模板类型是内置类型,也有同样的问题。
template<typename T>
void foo()
{
T x;
}
为了解决这个问题,需要显示地为内置类型调用一个默认构造函数将其初始化为零(or false for bool,or nullptr for pointer)。
template<typename T>
void foo()
{
T x{};
}
对于类的成员变量也是如此。
template<typename T>
class MyClass {
private:
T x;
public:
MyClass() : x{} {
}
...
};
C++11 开始,支持直接对非静态成员变量初始化。
template<typename T>
class MyClass {
private:
T x{};
...
};
使用 this->
对于派生类模板,调用基类的函数时,不一定是真正使用基类的该函数。例如:
class Base {
public:
void bar();
};
template<typename T>
class Derived : Base<T> {
public:
void foo() {
bar();
}
};
一种解决方法是使用基类的作用域限定:
template<typename T>
class Derived : Base<T> {
public:
void foo() {
Base<T>::bar();
}
};
另一种方法是使用 this-> :
template<typename T>
class Derived : Base<T> {
public:
void foo() {
this->bar();
}
};
原始数组和字符串字面值的模板
当传递原始数组或者字符串字面值给模板时需要格外小心。
如果模板参数申明为引用,入参不会发生退化(decay)。比如你传递一个 “hello”,它的类型为 char const[6] ,但是 “hello!” 的类型是 char const[7] ,二者不是同一个类型。
#include <iostream>
template<typename T>
const T& max(const T& a, const T& b)
{
return a < b ? b : a;
}
int main()
{
::max("hello", "world");
::max("hello", "world!");
}
如果是值传递,则会发生类型退化。例如字符串字面值会退化成 char const* 。但它的缺点是失去了数组长度的信息。
针对原始数组和字符串字面值,可以专门提供模板:
template<typename T, int N, int M>
bool less (T(&a)[N], T(&b)[M])
{
for (int i = 0; i<N && i<M; ++i) {
if (a[i]<b[i]) return true;
if (b[i]<a[i]) return false;
}
return N < M;
}
int x[] = {1, 2, 3};
int y[] = {1, 2, 3, 4, 5};
std::cout << less(x, y);
std::cout << less("ab", "abc");
如果只支持字符串字面值,可以将模板参数 T 替换为 const char :
template<typename T, int N, int M>
bool less (const char(&a)[N], const char(&b)[M])
{
for (int i = 0; i<N && i<M; ++i) {
if (a[i]<b[i]) return true;
if (b[i]<a[i]) return false;
}
return N < M;
}
对于边界未知的数组,可能需要重载和偏特化:
#include <iostream>
template<typename T>
struct MyClass;
template<typename T, std::size_t SZ>
struct MyClass<T[SZ]>
{
static void print() { std::cout << "print() for T[" << SZ << "]\n"; }
};
template<typename T, std::size_t SZ>
struct MyClass<T(&)[SZ]>
{
static void print() { std::cout << "print() for T(&)[" << SZ << "]\n"; }
};
template<typename T>
struct MyClass<T[]>
{
static void print() { std::cout << "print() for T[]\n"; }
};
template<typename T>
struct MyClass<T(&)[]>
{
static void print() { std::cout << "print() for T(&)[]\n"; }
};
template<typename T>
struct MyClass<T*>
{
static void print() { std::cout << "print() for T*\n"; }
};
template<typename T1, typename T2, typename T3>
void foo(int a1[7], int a2[],
int (&a3)[42],
int (&x0)[],
T1 x1,
T2& x2, T3&& x3)
{
MyClass<decltype(a1)>::print();
MyClass<decltype(a2)>::print();
MyClass<decltype(a3)>::print();
MyClass<decltype(x0)>::print();
MyClass<decltype(x1)>::print();
MyClass<decltype(x2)>::print();
MyClass<decltype(x3)>::print();
}
int main()
{
int a[42];
MyClass<decltype(a)>::print();
extern int x[];
MyClass<decltype(x)>::print();
foo(a, a, a, x, x, x, x);
}
int x[] = {0, 8, 15};
输出为:
print() for T[42]
print() for T[]
print() for T*
print() for T*
print() for T(&)[42]
print() for T(&)[]
print() for T*
print() for T(&)[]
print() for T(&)[]
成员模板
类的成员也可以是模板,包括嵌套类和成员函数。一般情况下,不能用不同类型的类互相赋值。
Stack<int> intStack1, intStack2;
Stack<float> floatStack;
...
intStack1 = intStack2;
floatStack = intStack1;
可以定义一个赋值运算符的模板来实现不同类型的赋值(通过合适的类型转换)。
template<typename T>
class Stack {
private:
std::deque<T> elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
template<typename T2>
Stack& operator= (Stack<T2> const&);
};
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
Stack<T2> tmp(op2);
elems.clear();
while (!tmp.empty()) {
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
为了获取用来赋值的源对象所有成员的访问权限,可以把其他的 stack 实例声明为友元。
template<typename T>
class Stack {
private:
std::deque<T> elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
template<typename T2>
Stack& operator= (Stack<T2> const&);
template<typename> friend class Stack;
};
这样,如下的赋值运算符实现就称为可能:
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
elems.clear();
elems.insert(elems.begin(),
op2.elems.begin(),
op2.elems.end());
return *this;
}
如下不同类型的类就可以相互赋值了:
Stack<int> intStack;
Stack<float> floatStack;
...
floatStack = intStack;
当然,v.emplace_front(tmp.top()); 会在编译时候检查类型转换是否合适,否则会编译报错。例如:
Stack<std::string> stringStack;
Stack<float> floatStack;
...
floatStack = stringStack;
更进一步,你还可以改变内部的赋值的容器实现:
template<typename T, typename Cont = std::deque<T>>
class Stack {
private:
Cont elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
template<typename T2, typename Cont2>
Stack& operator= (Stack<T2,Cont2> const&);
template<typename, typename> friend class Stack;
};
template<typename T, typename Cont>
template<typename T2, typename Cont2>
Stack<T,Cont>&
Stack<T,Cont>::operator= (Stack<T2,Cont2> const& op2)
{
elems.clear();
elems.insert(elems.begin(),
op2.elems.begin(),
op2.elems.end());
return *this;
}
成员函数模板的特化。
成员函数模板也能偏特化或全特化。
#include<iostream>
class BoolString {
private:
std::string value;
public:
BoolString (std::string const& s)
: value(s) {
}
template<typename T = std::string>
T get() const {
return value;
}
};
template<>
inline bool BoolString::get<bool>() const {
return value == "true" || value == "1" || value == "on";
}
int main() {
std::cout << std::boolalpha;
BoolString s1("hello");
std::cout << s1.get() << '\n';
std::cout << s1.get<bool>() << '\n';
BoolString s2("on");
std::cout << s2.get<bool>() << '\n';
return 0;
}
.template 标识
调用一个成员模板时,显式限定模板实参有时候是有必要的,使用关键字 template 来确保 < 是模板实参列表的开始。例如:
template<unsigned long N>
void printBitset (std::bitset<N> const& bs) {
std::cout << bs.template to_string<char, std::char_traits<char>,
std::allocator<char>>();
}
.template 只需要用于依赖于模板参数的名称之后,这里的 b 依赖于模板参数 N 。
泛型lambda和成员模板
C++14 引入的泛型 lambda,其实就是成员模板的简化。例如:
[] (auto x, auto y) {
return x + y;
}
class SomeCompilerSpecificName {
public:
SomeCompilerSpecificName();
template<typename T1, typename T2>
auto operator() (T1 x, T2 y) const {
return x + y;
}
};
变量模板
C++14 开始支持变量模板,也即变量的类型也可以参数化。例如:
template<typename T>
constexpr T pi{3.1415926535897932385};
和所有模板一样,这个声明不应该出现在函数或局部作用域内。使用如下:
std::cout << pi<double> << '\n';
std::cout << pi<float> << '\n';
可以在不同编译单元中申明和使用变量模板。
template<typename T> T val{};
#include "header.hpp"
int main()
{
val<long> = 42;
print();
}
#include "header.hpp"
void print()
{
std::cout << val<long> << '\n';
}
变量模板也可以有默认类型。例如:
template<typename T = long double>
constexpr T pi = T{3.1415926535897932385};
std::cout << pi<> << '\n';
std::cout << pi<float> << '\n';
std::cout << pi << '\n';
变量模板也支持非类型参数。
#include <iostream>
#include <array>
template<int N>
std::array<int,N> arr{};
template<auto N>
constexpr decltype(N) dval = N;
int main()
{
std::cout << dval<'c'> << '\n';
arr<10>[0] = 42;
for (std::size_t i = 0; i < arr<10>.size(); ++i) {
std::cout << arr<10>[i] << '\n';
}
}
变量模板的一个用法是为类模板成员定义变量。
例如,有一个类:
template<typename T>
class MyClass {
public:
static constexpr int max = 1000;
};
template<typename T>
int myMax = MyClass<T>::max;
auto i = myMax<std::string>;
auto i = MyClass<std::string>::max;
类似的例子:
namespace std {
template<typename T>
class numeric_limits {
public:
...
static constexpr bool is_signed = false;
...
};
}
template<typename T>
constexpr bool isSigned = std::numeric_limits<T>::is_signed;
isSigned<char>
std::numeric_limits<char>::is_signed
C++17 标准库就利用了变量模板简化了 type traits 的生成值
namespace std {
template<typename T> constexpr bool is_const_v = is_const<T>::value;
}
std::is_const_v<T>
std::is_const<T>::value
模板的模板参数
模板参数自己也可以是一个类模板。例如前面的栈类模板的例子,用模板的模板参数,可以只指定容器类型而不需要指定元素类型。
Stack<int, std::vector<int>> vStack;
Stack<int, std::vector> vStack;
因此,需要将第二个模板参数指定为模板的模板参数:
template<typename T,
template<typename Elem> class Cont = std::deque>
class Stack {
public:
void push(const T&);
void pop();
const T& top() const;
bool empty() const { return elems.empty(); }
private:
Cont<T> elems;
};
C++17 开始可以使用 typename 修饰模板的模板参数中的 Cont 。
template<typename T,
template<typename Elem> typename Cont = std::deque>
class Stack {
...
};
由于模板的模板参数中的模板参数没有被使用(Cont 没有用到模板参数 Elem ),因而可以简写为:
template<typename T,
template<typename> class Cont = std::deque>
class Stack {
...
};
模板的模板实参匹配
容器还有另一个参数,即内存分配器。
template<typename T,
template<typename Elem,
typename Alloc = std::allocator<Elem>>
class Cont = std::deque>
class Stack {
private:
Cont<T> elems;
...
};
Alloc 没被使用,也可以被省略。最终版本的 Stack 模板实现如下:
#include <deque>
#include <cassert>
#include <memory>
template<typename T,
template<typename Elem,
typename = std::allocator<Elem>
>class Cont = std::deque>
class Stack {
private:
Cont<T> elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
template<typename T2,
template<typename Elem2,
typename = std::allocator<Elem2>
>class Cont2>
Stack<T,Cont>& operator= (Stack<T2,Cont2> const&);
template<typename, template<typename, typename>class>
friend class Stack;
};
template<typename T, template<typename,typename> class Cont>
void Stack<T,Cont>::push (T const& elem)
{
elems.push_back(elem);
}
template<typename T, template<typename,typename> class Cont>
void Stack<T,Cont>::pop ()
{
assert(!elems.empty());
elems.pop_back();
}
template<typename T, template<typename,typename> class Cont>
T const& Stack<T,Cont>::top () const
{
assert(!elems.empty());
return elems.back();
}
template<typename T, template<typename,typename> class Cont>
template<typename T2, template<typename,typename> class Cont2>
Stack<T,Cont>&
Stack<T,Cont>::operator= (Stack<T2,Cont2> const& op2)
{
elems.clear();
elems.insert(elems.begin(),
op2.elems.begin(),
op2.elems.end());
return *this;
}
int main()
{
Stack<int> iStack;
Stack<float> fStack;
iStack.push(1);
iStack.push(2);
std::cout << "iStack.top(): " << iStack.top() << '\n';
fStack.push(3.3);
std::cout << "fStack.top(): " << fStack.top() << '\n';
fStack = iStack;
fStack.push(4.4);
std::cout << "fStack.top(): " << fStack.top() << '\n';
Stack<double, std::vector> vStack;
vStack.push(5.5);
vStack.push(6.6);
std::cout << "vStack.top(): " << vStack.top() << '\n';
vStack = fStack;
std::cout << "vStack: ";
while (! vStack.empty()) {
std::cout << vStack.top() << ' ';
vStack.pop();
}
std::cout << '\n';
}
输出如下:
iStack.top(): 2
fStack.top(): 3.3
fStack.top(): 4.4
vStack.top(): 6.6
vStack: 4.4 2 1
引用
|