C++ 模板进阶知识——万能引用
目录
- C++ 模板进阶知识——万能引用
- 1. 类型区别的基本含义
- 2. 基本认识
- 3. 判断题
- 4. 万能引用的示例
- 5. 万能引用资格的剥夺与辨认
- 5.1 剥夺
- 5.2 辨认
- 总结
C++ 模板进阶知识——万能引用
万能引用是C++11中引入的一个高级特性,它允许函数模板参数根据传入的实参自动成为左值引用或右值引用。这一特性极大地增强了模板函数的灵活性和通用性,使得编写可同时处理左值和右值的通用代码成为可能。
1. 类型区别的基本含义
看看下面这行代码:
void func(const int &abc){}
如果问:abc是什么类型?你可以脱口而出:const int &类型。这没错,因为在代码中写着呢!
现在,把这个func()
函数改造成一个函数模板:
template <typename T>
void func(const T &abc) { }
在main()
函数中添加代码调用一下:
func(10);
现在问题来了:T是什么类型?abc是什么类型?
答案是:
- T的类型是int;
- abc的类型是const int &。
通过观察,T的类型之所以是int,是因为进行函数调用的时候给的参数是10。这是对的,但不全面。T的类型不仅取决于调用时传入的参数,还取决于abc的声明方式(即const T &)。这种类型推导是理解万能引用的关键。接下来,我们探讨当abc的类型为万能引用时如何对T的类型产生影响。
2. 基本认识
Universal Reference(万能引用)后来被称为Forwarding Reference(转发引用)。万能引用是C++11引入的一个概念,它可以根据上下文自动成为左值引用或右值引用。万能引用是一种类型,与int类型类似。右值引用用&&
符号表示,主要绑定到右值上,如:
int &&rv = 1000;
来看一个例子:
void myfunc(int &&tmprv)
{
std::cout << tmprv << std::endl;
}
在main()
函数中添加代码:
myfunc(10); // 正确,右值作为实参
int i = 100;
myfunc(i); // 错误,右值引用不能绑定左值
将myfunc()
函数改造成函数模板:
template <typename T>
void myfunc(T&& tmprv)
{
std::cout << tmprv << std::endl;
}
编译后发现myfunc(i);
不再报错。原因是这里的tmprv
既能接受左值,也能接受右值,这就是万能引用。
万能引用存在两种语境:
- 必须是函数模板;
- 必须发生模板类型推断,并且函数模板形参形如T&&。
万能引用的格式为T&&,它与右值引用形式相同,但解释不同。右值引用作为函数形参时,实参必须传递右值;万能引用则可以绑定左值或右值。因此,万能引用更灵活,它可以变成左值引用或右值引用。
3. 判断题
判断以下参数类型是右值引用还是万能引用:
void func(int &¶m){...}
- 右值引用,因为
func()
不是函数模板。
- 右值引用,因为
template<typename T> void func(T&& tmpvalue) {...}
- 万能引用。
template<typename T> void func (std::vector<T>&¶m) {...}
- 右值引用。虽然有T&&,但T被嵌套在vector中,失去了直接的类型推导,因此不是万能引用。
4. 万能引用的示例
template <typename T>
void myfunc(T&& tmprv)
{
tmprv = 12; // 不管tmprv的类型是左值引用还是右值引用,都可以给tmprv赋值
std::cout << tmprv << std::endl;
}
int main()
{
int i = 100;
myfunc(i); // 左值被传递,因此tmprv是左值引用,执行完毕后i值变成12
i = 200;
myfunc(std::move(i)); // 右值被传递,因此tmprv是右值引用,执行完毕后i值变成12
}
通过以上内容,理解了万能引用的概念和应用。万能引用不仅与右值引用相似,但其存在的场景要求T是类型模板参数,并且后面跟随&&
。万能引用灵活且强大,可以根据实际情况绑定到左值或右值。
5. 万能引用资格的剥夺与辨认
万能引用(Forwarding Reference)是一个强大的特性,可以在不同的上下文中表现为左值引用或右值引用。然而,某些情况下,一个本应是万能引用的表达式会失去这个资格。理解何时发生这种剥夺以及如何辨认万能引用,对于高效利用C++的模板编程至关重要。
5.1 剥夺
万能引用的资格可以被以下几种情况剥夺:
-
类型说明符的存在:
-
如果在类型声明中使用了
const
、volatile
等类型修饰符,那么这个引用将不再是万能引用。例如,const T&&
不是万能引用,而是一个常量右值引用。 -
template<typename T> void func(const T&& param) {} // 这里的param是常量右值引用,不是万能引用
-
-
类型不直接为T&&:
- 如果类型通过别名(typedef或using)间接定义,那么它不再是万能引用。例如:
template<typename T> using myType = T&&; template<typename T> void func(myType<T> param) {} // 这里的param不是万能引用
- 如果类型通过别名(typedef或using)间接定义,那么它不再是万能引用。例如:
-
数组或函数类型:
-
如果T是数组或函数类型,那么即使使用了T&&的形式,也不是万能引用。
-
template<typename T> void func(T&& param) {} // 如果T是数组类型 int arr[10]; 则func(arr)中的param不是万能引用 // 如果T是函数类型 void f(); 则func(f)中的param不是万能引用
-
-
模板实例化时T已确定:
-
如果在模板实例化时类型T已经明确,则T&&不再具备万能引用的特性,而是变成了普通的右值引用。
-
template<typename T> void func(T&& param) {} int main() { int a = 10; func<int&>(a); // 在这里,T被显式指定为int&,所以T&&变为int& &&,即int& }
-
5.2 辨认
要辨认一个引用是否为万能引用,可以遵循以下规则:
-
检查是否在函数模板中:
- 确保正在查看的代码位于模板函数中,且涉及类型推断。
-
直接形式为T&&:
- 引用必须直接声明为
T&&
,其中T是模板参数,并且没有任何修饰(如const或volatile)。
- 引用必须直接声明为
-
无类型别名:
- 确保T&&没有通过类型别名定义。直接使用原始形式。
-
模板类型参数推断参与:
- 确保在函数调用时存在模板类型参数的推断。如果模板参数在函数调用前已确定,那么该引用就可能不是万能引用。
下面是一些例子来帮助辨认:
template<typename T>
void example1(T&& arg) { } // 万能引用
template<typename T>
void example2(const T&& arg) { } // 非万能引用,是const修饰的右值引用
template<typename T>
using RefType = T&&;
template<typename T>
void example3(RefType<T> arg) { } // 非万能引用,使用了类型别名
int main() {
int a = 10;
example1(a); // T被推导为int&
example1(10); // T被推导为int
}
总结
理解万能引用的关键在于:它依赖于类型推导,可以绑定到左值或右值,并且最终会根据传入的参数类型,变成左值引用或右值引用。