自动推导类型 auto
auto
关键字是C++11中的用法。在C++11之前,auto被用作声明自动存储局部变量。
最基本的,在C++11中,我们可以使用auto
关键字让编译器推导变量的类型,就像下面这个例子:
auto i = 42; // i被推导为int
auto d = 42.0; // d被推导为double
auto s = "hello"; // s被推导为const char*
编译器会根据初始化的值自动推导变量的类型。
然后,你需要注意的有:
auto
声明的变量在定义时必须初始化。auto
不能作为函数的形参类型。auto
不能直接声明数组。auto
不能定义类的非静态成员变量。
然而,auto
关键字一下情况中非常好用:
- 当变量类型很长或者复杂时,使用
auto
可以让代码更加简洁。 - 当处理模板时,使用
auto
可以简化依赖模板参数的变量声明。 - 函数模板依赖模板参数的返回值。
- 使用lambda表达式时,
auto
可以用于声明函数类型。
例如,假设我们有一个很长的类型,如std::unordered_map<std::string, std::vector<int>>
,使用auto
可以让我们的代码更简洁:
std::unordered_map<std::string, std::vector<int>> map;
// 无auto
for (std::unordered_map<std::string, std::vector<int>>::iterator it = map.begin(); it != map.end(); ++it) {
// ...
}
// 使用auto
for (auto it = map.begin(); it != map.end(); ++it) {
// ...
}
函数模板
函数模板是C++中用于实现泛型编程的重要工具,它可以创建通用函数,处理不同的数据类型,但其基本操作相同。这样可以提高代码的复用性和灵活性。
简单的,一个基本的函数模板看起来像这样:
template <typename T>
void Swap(T &a, T &b) {
T tmp = a;
a = b;
b = tmp;
}
其中template<typename T>
是模板声明,它告诉编译器我们将创建一个模板,并且我们将使用一个名为T
的类型参数。类型参数可以是任何有效地数据类型(如int
, double
, string
等等)。T
只是一个占位符,你可以用任何有效地标识符替换它。
在函数中,我们看到T
被用作变量a
、b
和tmp
的类型。这意味着这些变量可以接受任何类型,只要该类型支持赋值操作。
现在,我们可以使用这个函数模板来交换任何类型的两个变量,只要这两个变量的类型相同,并且该类型支持赋值操作。例如:
int x = 1, y = 2;
Swap(x, y); // 现在x=2, y=1
double m = 1.1, n = 2.2;
Swap(m, n); // 现在m=2.2, n=1.1
std::string s1 = "Hello", s2 = "World";
Swap(s1, s2); // 现在s1="World", s2="Hello"
注意点:
- 函数模板可以有多个类型参数。例如:
template <typename T, typename U> void foo(T t, U u) {...}
。 - 不是所有类型都可以用于函数模板。类型必须支持函数中使用的所有操作。例如,如果函数模板使用了除法操作,那么类型参数就必须支持除法操作。
- 模板类型参数可以显式指定,例如
Swap<int>(x, y);
,但通常让编译器自动推导是更好的选择。 - 可以为类的成员函数创建模板,但不能是虚函数和析构函数。
- 函数模板支持重载,可以有非通用数据类型的参数
函数模板具体化
函数模板具体化(特化)是一个特殊的函数模板,其可以为特定的类型提供特别处理的实现。在编译期,编译器会优先选择具体化的版本,而不是通用的模板版本。
下面是具体的例子展示:
template <typename T>
void print(T val) {
std::cout << "General template: " << val << std::endl;
}
template <>
void print<int>(int val) {
std::cout << "Specialized template for int: " << val << std::endl;
}
int main() {
print("Hello"); // 使用通用模板
print(10); // 使用特化模板
}
在上述例子中,我们创建了一个通用的print
函数模板,用于打印各种类型的值,同时也为int
类型提供了一个特化的print
函数模板,当我们调用print(10)
时,编译器会优先选择具体化模板,因为它为int
类型提供了更准确的匹配。
那么,怎么使用这种特性才能最大化其效果呢?在编译过程中,有时候你可能会遇到需要对某种类型做特殊化处理的情况,这时候就可以利用函数模板具体化。但是需要注意,具体化版本应该仅针对那些无法通过通用模板准确处理的函数,或者需要特殊操作的类型进行定义。
函数模板具体化需要注意一些要点包括:
- 具体化的函数模板必须在使用之前进行声明和定义。也就是说,你不能调用特化模板的代码之后才定义它。
- 当编译器在选择函数模板时,优先级是:最具体的匹配>模板具体化版本>通用的版本。
- 具体化版本并不是重载。重载发生在函数名相同但参数列表不同的情况,而具体化是针对特定类型的函数模板。
- 具体化的过度使用可能会导致代码变得复杂,难以阅读和维护。
- 不是所有类型都适合进行具体化。例如,虚函数和析构函数不能使用模板具体化。
函数模板分文件编写
由于模板的工作方式,导致模板的分文件编写存在一些困难和挑战。
模板不像常规函数和类一样的编译单元,当编译器遇到模板定义时,它并不会生成任何的代码。相反,当模板被实例化(即使特定的类型参数来使用模板)时,编译器才会生成代码。这就意味着模板的实现必须在其被实例化的地方可见,也就是说,在大多数情况下模板的定义需要放到头文件中。
然而,对于模板的具体化版本,情况有所不同。特化版本的模板在处理上更接近普通函数和类,其声明可以放在头文件中,而实现可以放在源文件中。
举个例子。假设我们有一个用于交换两个元素值的模板函数Swap
。这个函数模板可以放在头文件swap.h
中定义:
template <typename T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
然后,假设我们希望为字符串类型提供一个特化版本的Swap,因为交换两个字符串可能需要一些额外的操作(例如,记录交换操作的日志)。我们可以在头文件中声明这个特化版本,然后在源文件中实现它:
// 在swap.h中
template <>
void Swap<std::string>(std::string& a, std::string& b);
// 在swap.cpp中
#include "swap.h"
template <>
void Swap<std::string>(std::string& a, std::string& b) {
std::string temp = a;
a = b;
b = temp;
// 记录交换操作的日志...
}
这样,我们就能在任何包含了swap.h
的源文件中使用通用的Swap
模板函数,也能使用为string
特化的Swap
函数。这就是函数模板分文件编写的基本原理和实践方法。
总结就是,模板定义通常放在头文件中,而对于具体化的模板,其声明可以放在头文件中,实现可以在源文件中。然而,这种做法可能需要根据具体的编译器或编译环境进行调整。
函数模板高级应用
decltype关键字
decltype
关键字用于查询表达式的类型,这在函数模板中非常有用,他可以根据输入参数的类型推断返回类型。
#include<iostream>
using namespace std;
template<typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
return t1 + t2;
}
int main()
{
int i = 1;
double d = 2.2;
cout<<add(i, d)<<endl; // 输出3.2
return 0;
}
上述例子中,add()
使用了decltype
关键字,向函数传递了一个int和一个double,那么有了decltype
就会返回类型double。
函数后置返回类型
int func(int x,double y);
等同:
auto func(int x,double y) -> int;
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
return t1 + t2;
}
上述例子中,函数add
使用了后置返回类型,允许在函数名后面声明返回类型。这是因为在模板函数中,我们可能希望返回类型取决于模板参数。