typename关键字
当提到typename关键字时,首先想到的便是和class关键字一样,定义模板时引入一个模板参数,但除此之外,typename关键字还拥有另外一个功能——消除歧义:指明模板内部的一个标识符代表的一种类型,而不是一个静态成员,例如:typename T::SubType * ptr表示定义一个类型为T::SubType的指针变量,而不是T类型的静态成员变量和ptr相乘。先来看一个例子:
template <typename T>
long long Sum(const T &cont)
{
long long sum = 0;
T::const_iterator iter;
for (iter = cont.begin(); iter != cont.end(); ++iter)
sum += *iter;
return sum;
}
上面这段代码看似合情合理,然而却编译失败了,错误如下:
missing 'typename' prior to dependent type name 'T::const_iterator'
错误的意思是“嵌套类型'T::const_iterator'缺少typename关键字”。为什么必需要使用typename关键字?这与模板的编译机制有关。在模板实例化之前,编译器首先要对模板进行语法解析,但此时模板没有实例化,编译器无法确定T指向的具体类型,也就无法确认T::const_iterator到底是一个嵌套类型,还是一个静态成员变量,因此也就产生了歧义。将“T::const_iterator iter;”改为“typename T::const_iterator iter;”,告诉编译器T::const_iterator是一个嵌套类型,歧义就消除了。
即使模板嵌套类型用于返回值类型,也需要使用typename消除歧义。但这种情况下,编译器给出的是警告,而非错误。如下面的代码,编译器给出的就是警告“missing 'typename' prior to dependent type name T::size_type; implicit 'typename' is a C++20 extension [-Wc++20-extensions]”
template <typename T>
T::size_type GetContainerSize(const T &container)
{
return container.size();
}
零初始化
对于内置类型,比如 int,double 以及指针类型,由于它们没有默认构造函数,因此它们不 会被默认初始化成一个有意义的值。对于这些类型,只能通过显示的方式完成初始化,例如:
char *s = nullptr;
int i = 0;
double d = 0.0;
bool b = false;
C++11之前尽管可以通过类似初始化对象的方式初始化内置类型变量,但仅限于类的成员变量,并且在构造函数的初始化列表使用,如下:
class zero_init_class
{
public:
zero_init_class(void)
: s()
, b()
, i()
, a() {
}
private:
char *s;
bool b;
int i;
char a[3];
};
但如果对于函数内的局部变量,上面的方式无法使用,例如:
char *s(); //empty parentheses interpreted as a function declaration, replace parentheses with an initializer to declare a variable
//char *s();
// ^~
// = nullptr
bool b1(); //empty parentheses interpreted as a function declaration, replace parentheses with an initializer to declare a variable
//bool b1();
// ^~
// = false
因此,C++11之前在函数模板或类模板的成员函数中,模板类型局部变量的初始化的有效方式如下:
T val = T();
从C++11开始,变量的初始化方式进行简化,并且普通变量和指针的初始化进行了统一,如下:
char *s{};
bool b{};
int i{};
char a[3]{};
std::string str{};
使用this->
下面的源码看似没有任何问题,但执行结果(::bar)却让人出乎意料:
#include <cstdio>
void bar(void) {
printf("::bar\n");
}
template <typename T>
class Base {
public:
void bar(void) { printf("bar\n"); }
};
template <typename T>
class Derived : public Base<T> {
public:
void foo(void) { bar(); }
};
int main(int argc, char **argv) {
Derived<int> d;
d.foo();
}
之所以会这样,Derived 中的 bar()永远不会被解析成 Base 中的 bar()。如果没有全局函数bar,编译其就会报错:Use of undeclared identifier 'bar'。但下面的源码便能够正常通过编译:
#include <cstdio>
/*void bar(void) {
printf("::bar\n");
}*/
template <typename T>
class Base {
public:
void bar(void) { printf("bar\n"); }
};
//template <typename T>
class Derived : public Base<int> {
public:
void foo(void) { bar(); }
};
int main(int argc, char **argv) {
//Derived<int> d;
//d.foo();
Derived d;
d.foo();
}
由此可以推断,这种情况的出现可能与模板的编译方式相关,并且该错误出现实例化之前,由于未确定Base<T>的具体类型,不确定Base<T>是否存在bar成员函数。 对于这种情况,可以使用如下的两种解决方案:
void foo(void) { this->bar(); }
void foo(void) { Base<int>::bar(); }
成员模板
类的成员也可以是模板,对嵌套类和成员函数都是这样。类型成员作为模板有什么意义呢?先看下面的一个的例子:
Stack<int> intStack1, intStack2; // stacks for ints
Stack<float> floatStack; // stack for floats
...
intStack1 = intStack2; // OK: stacks have same type
floatStack = intStack1; // ERROR: stacks have different types
默认的赋值运算符要求等号两边的对象类型必须相同,因此,floatStack = intStack1无法通过编译。为了使floatStack = intStack1能够成功通过编译,需要将赋值运算符定义成模板,如下:
//本代码仅仅是一个事例,并非所有的容器都会支持push_back,back,pop_back等方法
template <typename T, typename C = std::vector<T>>
class Stack {
private:
C elems;
public:
void push(const T &data) { elems.push_back(data); }
void pop() { elems.pop_back(); }
T const& top() const { return elems.back(); }
bool empty() const { return elems.empty(); }
template<typename T2, typename C2>
Stack& operator=(Stack<T2, C2> const &rhs);
//为了赋值时方便访问rhs对象的elems成员,无法确定rhs和当前对象是否属于同一类型
template<typename, typename> friend class Stack;
};
template <typename T, typename C>
template <typename T2, typename C2>
Stack<T, C> &Stack<T, C>::operator=(Stack<T2, C2> const &rhs)
{
elems.clear();
elems.insert(elems.begin(), rhs.elems.begin(), rhs.elems.end());
return *this;
}
变量模板
从 C++14 开始,变量也可以被某种类型参数化,称为变量模板。
//val_define.hpp
#ifndef _val_define_
#define _val_define_
template<typename T>
T val{};
#endif
//val_print.hpp
#ifndef _val_print_
#define _val_print_
#include "val_define.hpp"
#include <iostream>
void print(void)
{
std::cout<<val<long><<std::endl;
}
#endif
//val_template.cpp
#include "val_print.hpp"
int main(int argc, char **argv) {
val<long> = 42;
print();
}
应用场景有二:
- 用于书成员的变量模板,例如:bool is_signed = std::numeric_limits<unsigned long>::is_signed;。实现见标准库<limits>
-
类型萃取 Suffix_v,例如:bool is_const2 = std::is_const<int>::value;。实现见标准库<is_const.h>
模板模板参数
模板参数也可是是一个模板,下面一个具体的实现:
#include <cstdio>
#include <vector>
//本代码仅仅是一个事例,并非所有的容器都会支持push_back,back,pop_back等方法
template <typename T, template<typename U, typename Alloc = std::allocator<U>> class C = std::vector>
class Stack {
private:
C<T, std::allocator<T>> elems;
public:
void push(const T &data) { elems.push_back(data); }
void pop() { elems.pop_back(); }
T const& top() const { return elems.back(); }
bool empty() const { return elems.empty(); }
template <typename T2, template<typename U2, typename Alloc2> class C2 = std::vector>
Stack& operator=(const Stack<T2, C2> &rhs);
//为了赋值时方便访问rhs对象的elems成员,无法确定rhs和当前对象是否属于同一类型
template <typename, template<typename, typename> class> friend class Stack;
};
template <typename T, template<typename U, typename Alloc> class C>
template <typename T2, template<typename U2, typename Alloc2> class C2>
Stack<T, C> &Stack<T, C>::operator=(Stack<T2, C2> const &rhs)
{
elems.clear();
elems.insert(elems.begin(), rhs.elems.begin(), rhs.elems.end());
return *this;
}
int main(int argc, char **argv)
{
Stack<int, std::vector> stack;
stack.push(0);
stack.push(1);
Stack<float, std::vector> stackf;
stackf = stack;
printf("%f\n", stackf.top());
stackf.pop();
stackf.pop();
return 0;
}
当使用模板作为模板参数后,变量定义由Stack<int, std::vector<int>> stack变为Stack<int, std::vector>,容器vector的元素类型只需指定一次,这样变量的定义更加简洁。由于模板参数模板中的模板参数没有被用到,可以被省略,由template <typename T, template<typename, typename> class C = std::vector>变为template <typename T, template<typename, typename> class C>。
但有一点需要注意:直到c++17,template <typename T, template<typename, typename> class C>中的class才可以替换成typename,在C++17之前,会产生编译警告:
template template parameter using 'typename' is a C++17 extension [-Wc++17-extensions]
template <typename T, template<typename U, typename Alloc = std::allocator<U>> typename C = std::vector>