引用
在目前编程语言中,一般变量类型可以分类为
- 值类型
- 指针类型
- 引用类型
目前很多语言中只有引用类型,比如java出了基本类型以外都是引用类型,但是C+ + 都用这三种类型,所以C+ +在这方面要复杂不少,而且出现了右值引用这个概念。
左右值
C+ +可以将变量分为左值和右值,简单的说C+ +左值就是具有地址的值,右值是不具有地址的值。如何区分他们呢。
通常情况下:
- 常类字面量是右值 如 111, “hello world”
- 函数返回值,返回左值引用除外
- 构造无名对象
我们发现这些类型都是 临时的,即将消亡的,生命周期很短暂,所以可以成为将亡值;同时没有地址,不可以被取地址,更不可被左值引用;右值还有一个特点就是无法修改。
std::string getHello()
{
return {"Hello, World!"};
}
int main()
{
std::cout << "Hello, World!" << std::endl;//右值
std::cout << getHello() << std::endl;//右值
std::cout << std::string("Hello, World!") << std::endl;//右值
return 0;
}
为什么需要右值引用
本篇都是用构造函数举例,但是实际上不仅仅只有构造函数需要右值.
存在一个buffer类
class buffer
{
//.....
private:
int capacity;//容量
int len;//长度
unsigned char* buf;
}
类似与一个简化版本的 vector. 分析如下
函数传入参数的的时候,我们可以选择 值传递和引用传递,值传递有一个致命的缺点,就是会发生一次拷贝构造。解决办法就是加入引用
void dothing(buffer b) // buffer&
{
}
int main()
{
buffer buf(5);
dothing(buf);
return 0;
}
但是就会但是另一个问题,右值是无法被引用的!也就说这个函数无法接受右值。
void dothing(buffer& b);
dothing(buffer(10)); //报错
为了让我们这个参数接受右值,我们有一个解决办法就是 const 修饰
void dothing(const buffer& b)
好了,现在我们的函数既可以接受左值,同时可以接受右值。
为啥被const修饰之后就可以接受右值了呢?
生命周期的角度去看
由于右值是将亡值,无法被左值引用,C++有一个特性,const的左值引用遇到 一个将亡值,会将将亡值的生命周期延长为这个引用的生命周期的长度。
也就说 const 左值引用是可以 接受右值的
虽然我们可以可以既可以做到接受右值,也可以做到接受左值,但是这并无法满足所有情况。比如这样我们无法在函数体内部修改 const 引用,另外右值既然是将亡值,那么他们的资源即将就要被销毁,我们是否可以利用这个资源呢?
一个典型的例子就是copy构造, 我们发现深拷贝一个对象开销不小的,但是如果是右值的传入,我们也许不需要重新开辟控件和copy数据,反正都要将亡,那么我们直接占用就好了,很可惜我们无法修改const 引用哇,所以c++添加右值引用规则
buffer(const buffer & other)
{
std::cout<<"拷贝构造\n";
this->capacity = other.capacity;
this->len = other.len;
this->buf = new unsigned char[capacity] {};
std::copy(other.buf, other.buf + other.capacity, this->buf);
}
右值的好处
buffer(buffer && other)
{
std::cout<<"move拷贝构造\n";
this->capacity = other.capacity;
this->len = other.len;
this->buf = other.buf;
other.buf= nullptr;
}
对比左值的copy 这样优化了不少。
copy & swap
一个类 如果我们需要 拷贝和复制,那么我们需要写出如下版本的函数。
如果我们不需要,使用delete 阻止编译器给我们添加默认的版本
buffer& operator=(buffer const& other) = delete;
- 拷贝构造函数
buffer(const buffer & other)
- 移动构造函数
buffer(buffer && other)
- 拷贝赋值函数
buffer& operator=(const buffer& other)
- 移动拷贝赋值函数
buffer& operator=(buffer&& other)
而且代码重复了,那么我们有没有一个简单的方法呢?
swap & copy 模式,可以减少一个函数的代码,我们声明了一个swap函数,很简答就是为了交换俩个buffer。
buffer(buffer& buffer)
:capacity(buffer.capacity), len(buffer.len),
buf(capacity ? new unsigned char[capacity] {0} : nullptr)
{
if(capacity)
std::copy(buffer.buf, buffer.buf + buffer.capacity, this->buf);
}
buffer& operator=(buffer other)noexcept //这里时值传递 无论左值 右值都可以接收
{
Swap(*this, other);
return *this;
}
buffer(buffer&& buffer):noexcept :capacity(0), len(0), buf(nullptr)
{
Swap(*this, buffer);
}
static void Swap(buffer& lhs, buffer& rhs)noexcept
{
std::swap(lhs.buf, rhs.buf);
std::swap(lhs.capacity, rhs.capacity);
std::swap(lhs.len, rhs.len);
}
值得说明的是 buffer& operator=(buffer other)noexcept
传入是左值:
- 调用一次拷贝构造,这正是我们想要的,代替了手动copy
- swap霸占
传入右值:
- 调用一次move拷贝构造,注意不是拷贝构造函数,移动构造仅仅swap
- swap霸占
- 也只是多了一次交换过程,所以称不上逆向优化
std::move
我们在在代码中经常遇到一个需求,我们知道这个对象即将不会被使用了,我们需要将左值变为右值,std::move 就一个将一个左值 显示变为右值。
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
//typename std::remove_reference<_Tp> 是去掉 引用的 萃取器
//简化
template<typename _Tp>
_Tp&& move(_Tp&& __t) noexcept
{
return static_cast<_Tp&&>(__t);
}
static_cast
C++的类型转换
完美转发
引入了新的特性,但是总会存在新的需求,这就是所谓 问题越解决越多,熵是不可以减少的。
解释一下什么是完美转发,它指的是函数模板(泛型编程中遇到的问题)可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美**,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变**。
template<typename T,typename Arg>
std::shared_ptr<T> make_shared_prt(Arg arg)
{
return std::shared_ptr<T>(new T(arg));
}
我们调用make_shared_prt
函数,就和直接调用return std::shared_ptr<T>(new T(arg))
的感觉一样,这就是完美转发。
一般转发
为啥需要保证被转发参数的左、右值属性不变,因为T的构造器可能右多个版本,我们希望我们make函数可以更具我们传递的值自动调用对应版本的构造函数.但是一般情况下可能吗?
就拿我们这个例子来说, 即使我们重载了一个右值的版本
template<typename T,typename Arg>
std::shared_ptr<T> make_shared_prt(Arg&& arg) {
return std::shared_ptr<T>(new T(arg))
}
但是我们调用 T的构造器的时候, arg是一个左值, 因为在函数内部他已经具有一个名字了.
显然一般情况下,我们是完成我们上述的完美转发的需求的
引用折叠
//T& & -> T&
//T& && -> T&
//T&& && -> T&
//T&& && -> T&&
只有 && && 才能得到 && ,下面可以拿到编译去做一个实验
using T=int;
using T1=T&;
using T2=T&&;
using T3=T1&; //T& & -> T&
using T4=T1&&;//T& && -> T&
using T5=T2&;//T&& && -> T&
using T6=T2&&;//T&& && -> T&&
万能引用
前面我说让C++一个函数既可以接受左值,也可以接受右值, 可以使用const& ,其实还有一个办法,使用万能引用.
template<typename T>
void fun(T&& t)
{
std::cout<<"万能引用\n";
}
int i=0;
fun(1);
fun(i);
所谓万能引用就是既可以接受左值,同时可以接受右值. 这个必须是模板形式的 T&&
,如果不是模板那么不能说是万能引用.到这个现象的原因是引用折叠.
-
如果我们传入左值 , 那么模板T会被展开为 T&
-
如果是右值 ,那么 模板T汇编展开为T&&
从而能够接受左值和右值,
std::forward
如何使用 :
template<typename T,typename Arg>
std::shared_ptr<T> make_shared_prt(Arg&& arg) {
return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
这样我们就可以让 arg 被转发参数的左、右值属性不变。
原理是什么呢 ,看源码:
//两个模板
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
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);
}
·
//化简
//匹配左值
//std::remove_reference<_Tp> 去掉引用的萃取器
template<typename _Tp>
_Tp&&
forward(_Tp& T) noexcept
{ return static_cast<_Tp&&>(__t); }
//匹配右值
template<typename _Tp>
_Tp&&
forward(_Tp&& T) noexcept
{ return static_cast<_Tp&&>(__t); }
当传入左值的时候
//Tp 展开为 Tp&
template<typename _Tp>
_Tp&& //-> Tp&& & -> tp&
forward(_Tp& T) noexcept
{ return static_cast<_Tp &&
//-> Tp&& & -> tp&
>(__t); }
当传入右值
//Tp 展开为 Tp&&
template<typename _Tp>
_Tp&& //-> Tp&& && -> tp&&
forward(_Tp& T) noexcept
{ return static_cast<_Tp&&
//-> Tp&& && -> tp&&
>(__t); }