大家好,我叫徐锦桐,个人博客地址为www.xujintong.com。平时记录一下学习计算机过程中获取的知识,还有日常折腾的经验,欢迎大家访问。
在写STL源码的时候遇到的问题,在这里写一篇笔记记录一下。
一、引用折叠
引用折叠表示了一个值被引用多次时(只有在模板推导时候),会生成什么类型。
T& & 折叠成 T&
T& && 折叠成 T&
T&& & 折叠成 T&
T&& && 折叠成 T&&
其实总结起来就是,只有两次都是右值引用的时候才是T&&,即T&& && 折叠成 T&&
,其他都是转换成左值引用T&。
二、左值引用
左值引用,就是个别名,将一个变量绑定到另一个变量上。
int a = 10;
int &b = a; // 左值引用
const int &c = 10; // 常量左值引用
b = 20;
std::cout << a << std::endl; // 20
修改引用变量会影响原来的变量的值,但是 const int & 不能修改原对象的值(中间的int可以更换为其他类型)。
一般来说,函数传参的时候是拷贝传参,也就是将变量复制到一个临时变量,再将临时变量传入函数,最后销毁临时变量。这里涉及到了内存的拷贝,消耗的时间比较长。但是如果在传参的时候使用左值引用,不会涉及到内存的拷贝,因为是别名,相当于直接修改原变量了。
void left_value(int& x) {
x = 200;
}
int main()
{
int value = 10;
left_value(value);
std::cout << value << std::endl; // 200
return 0;
}
当然如果是const&
的话,虽然不涉及到内存的拷贝过程,但是不能修改原变量。
void left_value(const int& x) {
x = 200; // error: assignment of read-only reference 'x'
}
int main()
{
int value = 10;
left_value(value);
std::cout << value << std::endl; // 200
return 0;
}
这里函数传参的时候,为什么 int 能匹配到 int& 或者 const int& 呢?
当你将一个int
类型传递给int&
参数时,c++会执行一个类型转换,将int
隐式的转换为int&
,另一个同理。注意:int、int&、const int& 在匹配函数的时候优先级一样,同时出现会发生歧义错误。
函数传参的时候隐式转换很常见。
void left_value(int num) {
std::cout << num << std::endl;
}
int main()
{
float a = 1.5;
left_value(a); // 1
return 0;
}
传入的时候隐式的将float类型转化为int类型。
但是用函数重载就可以解决这个问题。
void left_value(int num) {
std::cout << num << std::endl;
}
void left_value(float num) {
std::cout << num << std::endl;
}
int main()
{
float a = 1.5;
left_value(a);
return 0;
}
会调用最适合的函数,那如果没有的话就只能进行隐式的类型转换了。
三、右值引用
右值引用是c++11引用的新特性。就是左值引用是给一个变量加别名,而右值引用就是绑定变量到一个临时值上,临时变量的生命周期和新左值的生命周期绑定,原来的临时变量销毁。
int &&d = 10; // 右值引
d = 200; // 此时 d 是变量是左值
引用在c++中是一个特别的类型,因为它的值类型和变量类型不一样, 左值/右值引用变量的值类型都是左值, 而不是左值引用或者右值引用。 这句话意思非常重要,简单来说,看上面代码,一开始将d绑定到了10上面,然后又将d赋值,在int &&d=10 之后,每次使用d,d都是作为一个左值使用的。一个右值引用变量在使用上就变成了左值,已经不再携带其是右引用这样的信息,只是一个左值。
右值引用也是提高性能用的。
vector<string> v;
string s = "teststring";
v.push_back(s);
上面这个代码,用了一个临时变量。假如我们啥都不干那么是怎么进行的呢,首先会先创建一个临时字符串,然后将这个临时字符串拷贝到v的内存上,然后再销毁这个临时字符串。中间进行内存的开辟和销毁,在一般数据上性能没啥问题,如果是数据特别多的话,性能就会严重下降。
但如果我们用右值引用,如下代码。
vector<string> v;
string s = "teststring";
v.push_back(std::move(s));
这里的std::move()
是将一个左值强制转换为右值,下面我会讲,现在就知道能将左值强制转化为右值就行了。
中间的过程就变成了,创建一个临时字符串,然后直接将这个临时字符串挂载到v上面。是不是中间少了好多过程,性能也大大的提高了。
四、万能引用
模板编程中,有的时候需要传入左值也传入右值引用。
下面这个只能传入一个左值,如果传入右值就会报错。
template <typename T>
void func(T& value) {
std::cout << "函数调用" << std::endl;
}
int main()
{
int value = 10;
func(value);
func(10); // 报错
return 0;
}
这时候就用到了我们的万能引用T&&。
template <typename T>
void func(T&& value) {
std::cout << "函数调用" << std::endl;
}
int main()
{
int value = 10;
func(value);
func(10);
return 0;
}
解释一下:
能同时传入左值引用和右值引用。
如果是左值,T先会推导成T&,然后再发生引用折叠,如果是右值引用,T会推导成T&&。
比如说,func(value)
中的value是一个左值,传入后T会推导成T&,然后再后后面的&&发生折叠引用,就会变成T&。
func(10)
中的10是一个右值,T会推导成T&&,然后再和后边的&&发生引用折叠,最后变成了T&&。
**T&&只在
五、move
std::move
它其实啥都没干,就是强制将左值转化为右值。
show you the code。
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
这里面的 std::remove_reference<_Tp>
,就像去除变量的引用,假如_Tp是int&&,最后返回type是个int。
std::remove_reference<_Tp>
的源代码如下。
/// remove_reference
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
template<typename _Tp, bool = __is_referenceable<_Tp>::value>
struct __add_lvalue_reference_helper
{ typedef _Tp type; };
template<typename _Tp>
struct __add_lvalue_reference_helper<_Tp, true>
{ typedef _Tp& type; };
六、完美转发
完美转发是配合万能引用使用的,通过万能引用传入左值和右值的参数,然后通过完美转发保留这个属性,转发给对应的重载函数。
show the code。
/*
函数模板的重载
*/
template <typename T>
void to_forward(T& value) {
std::cout << "调用的左值函数" << std::endl;
}
template <typename T>
void to_forward(T&& value) {
std::cout << "调用的右值函数" << std::endl;
}
template <typename T>
void func(T&& arg) {
to_forward(arg); // 调用左边值函数
}
int main(){
int value = 10;
func(std::move(value));
}
在这个代码中,func函数中的to_forward
函数最终会调用to_forward(T& value)
,虽然传入的是右值,一个右值引用变量在使用上就变成了左值,已经不再携带其是右引用这样的信息,只是一个左值,所以这里调用的是左值的重载函数。
首先展示一下std::forward
的源码。
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
这里有两个重载的函数,一个对应左值一个对应右值。
forward(typename std::remove_reference<_Tp>::type& __t)
这个是先将_t的类型去掉引用,然后加上个&,也就是最后是左值类型。
那下面forward(typename std::remove_reference<_Tp>::type&& __t)
那个肯定就是右值的了。
这里也用到了引用折叠,连个返回的方法是一样的,都是return static_cast<_Tp&&>(__t)
,如果_Tp是&,然后在和后面的&&折叠就是左值了;如果_Tp是&&,然后再和后面的&&折叠还是&&。
std::move和std::forward其实什么都没做,只是强制转换了一下类型。
看下面这个代码,这个是正确的。我们通过完美转发保留了这个左值还是右值的属性,然后再传给另一个函数。
#include <iostream>
#include <utility>
/* 完美转发 + 万能引用 + std::move */
/*
函数模板的重载
*/
template <typename T>
void to_forward(T& value) {
std::cout << "调用的左值函数" << std::endl;
}
template <typename T>
void to_forward(T&& value) {
std::cout << "调用的右值函数" << std::endl;
}
/*
万能引用:
能同时传入左值引用和右值引用
如果是左值,T先会推导成T&,然后再发生引用折叠
入股是右值引用,则会什么都不干
利用完美转发std::forward:
先通过万能引用可以传入左值引用和右值引用
然后通过完美转发(能保留传入时候是左值引用还是右值引用的属性,然后转发到对应的函数重载中
*/
template <typename T>
void func(T&& arg) {
// 保留左值和右值的属性
to_forward(std::forward<T>(arg));
}
int main() {
int value = 10;
int& value_l_refernce = value;
func(value); // 调用的左值引用函数
func(value_l_refernce); // 调用的左值引用函数
func(100); // 调用的右值引用函数
int&& value_r_refernce = 30;
// 右值引用使用后,之后调用这个变量都是作为左值
func(value_r_refernce); // **调用的左值引用**
func(std::move(value)); // 调用的右值引用
return 0;
}