html5手机网站织梦模板,鲜花网站建设的总体目标,在西部数码做的企业网站不能与阿里巴巴网站相连接,大作业网站建设方案你能学到
左值 与 右值左值引用 与 右值引用 基本用法与作用拷贝构造函数 与 移动构造函数移动语义 与 std::move万能引用 与 引用折叠完美转发#xff1a;std::forward 前言
本文代码片段中变量命名规则如下#xff1a;
小写字母#xff1a;一般类型的变量#xff08;非…你能学到
左值 与 右值左值引用 与 右值引用 基本用法与作用拷贝构造函数 与 移动构造函数移动语义 与 std::move万能引用 与 引用折叠完美转发std::forward 前言
本文代码片段中变量命名规则如下
小写字母一般类型的变量非指针、非引用ppointer指针类型的变量p1、p2…rreference引用类型的变量 lr_*比如 lr_aa 的左值引用rr*右值引用rr1、rr2… 上述均是非 const 变量如果名称以 c 开头说明该变量为 constfunfunction函数名fun1、fun2…大写字母自定义类名 在说左值引用与右值引用之前有必要先说说什么是左值什么是右值 c 中的左值与右值没有标准的定义没必要死套公式、死扣细节只需要理解即可。 如果你想要了解更多可以通过关键字 c value categories 搜索相关资料。 1. 左值与右值
左值lvalue右值rvalue
就它俩的英文缩写以及中文翻译使得有一部分人将其解释为 lvalue: left value rvalue: right value 因此也诞生了对左值与右值的一种解释 左值能出现表达式左边 右值不能出现在表达式左边 这种解释从某方面来说也可以是对的。下面我从另外一方面来解释什么是左值什么是右值 1.1 左值 lvalue 是 loactor value 的缩写 左值指的是 存储在内存中有明确内存地址的数据。因此在语法层面
左值包含两部分信息:
内存地址记录对象在内存中的位置数据值记录对象的值
所以它可以 出现在表达式的左边 出现在表达式的右边 取地址
int a 0; // a 为左值值为 1int b a; // 正确a 能出现在右边
a 1; // 正确a 能出现在左边
a; // 正确a 能取地址【总结】 左值 可以取地址的、有名字的、非临时的它是用户创建的能通过作用域的规则知道它的生存周期。 1.2 右值 lvaue 是 read value 的缩写 右值指的是 可以提供数据值的数据。在语法层面
右值仅包含数据值因此它 只能出现在表达式左边 不可以取地址
int a 0, b 1;100; // 100 是一个右值
100 a; // 错误右值不能出现在左边
100; // 错误右值不能取地址【总结】 右值 不能取地址、没有名字、临时的它的创建与销毁实际由编译器在幕后控制对用户而言有用的信息仅仅是它的数据值。 【扩展】 在 c 中右值实际分为两种 纯右值 (prvalue) 返回类型不是引用的一般函数的返回值 int fun() { int a 1; return a; }
fun() 1; // 错误fic() 返回值为纯右值运算表达式的结果 int a 0, b 1;
a b 1; // 错误运算表达式 a b 返回的是纯右值原始字面量lambda 表达式取地址操作… … 将亡值 (xvalue就字面意思即将死亡的临时对象) 返回类型为右值引用的函数的返回值std::move 的返回值… … 总的来说最直接地区分左值与右值的方法为是否可以取地址。 2. 左值引用与右值引用 此部分讲解的主要内容在于用左值或者右值给左值引用与右值引用进行赋值不会讲解如果加了 const 修饰需要注意什么问题。如果你对后者感兴趣看见作者的另外一篇文章C const 关键字详解 无论是左值引用还是右值引用在语法层面都是 给对象起别名与被绑定的对象共用同一块内存。在这里先记住一个结论 左值引用视为左值右值引用 有名称的 视为左值否则 视为右值 2.1 左值引用
顾名思义是对左值的引用给左值起别名。 语法类型 名称 左值 遵循以下规则
非 const 的左值引用 只能用 左值 来赋值。const 的左值引用 可以用 右值 来赋值。 看到这里你可能会疑惑左值引用不是只针对左值吗为什么还能用右值来赋值别急往后看。 // 以下变量都是左值
int a 0;
int* p a;
int r a;// 以下都是给左值起别名
int r_a a;
int* r_p p; // 对 int* 的引用类型解析从右到左/*** 直观上 r_r 是 r 的别名* 但由于 r 是 a 的别名* 因此 r_r 也可以看作 a 的别名 */
int r_r r; // 特殊
int r1 100; // 错误非 const 左值引用不能用右值初始化
const int cr 100; // 正确const 左值引用能用右值初始化对于
const int cr 100;这可能就会让人感到疑惑 为什么左值引用 (const) 能引用右值 先来看一个例子
int fun(int a) { }如果我们尝试如下调用这个函数
fun(0);会报错
这是因为 1 是一个右值而 非 const 的左值引用 不能用右值初始化。在 c11 以前不存在 右值引用的概念因此为了解决这一问题引入了规则 const 的左值引用 既可以用 左值 赋值也可以用 右值 赋值。 所以当参数类型为左值引用时更建议使用 const 左值引用也是出于这个考虑。 比如在老版本的 vector 模板的 push_back() 方法
void push_back(const value_type __x);vectorint t;
int a 1;// 以下均正确
t.push_back(a);
t.push_back(1);2.2 右值引用
顾名思义对右值的引用给右值起别名。 语法类型 名称 右值 遵循以下规则
右值引用 只能用 右值 初始化
int a 1, b 1;
int* p a;
int lr a;
int rr 100;
int fun() { return 0 }// 以下均是右值
100;
a b;
a;// 以下均正确
int rr1 100;
int rr2 a b;
int* rr3 a; // 对 int* 指针的右值引用
int rr4 fun(); // fun() 的返回值为 int无名称视为右值// 以下均错误
int rr5 a; // a 是左值
int rr6 p; // p 是指针为左值
int rr7 lr; // lr 为左值引用视为左值
int rr8 rr; // rr 有名称右值引用视为左值从
// 以下均错误
int rr6 lr; // lr 为左值引用为左值
int rr7 rr; // rr 是右值引用为左值也验证了之前所说的 左值引用视为左值右值引用 有名称的 视为左值否则 视为右值 既然 有名称的右值引用 是左值它是用右值初始化的。也就是说 右值引用使得右值 “重获新生”让此右值的生命周期 与 对应的右值引用的生命周期一样只要该右值引用还活着该右值也将一直存活下去。 基本语法搞定之后那么你会好奇这么一个问题 既然左值引用已经解决了用右值给左值引用初始化的问题那为什么还要引入 右值引用 呢 这是 为了性能考虑。 3. 移动语义 与 std::move move可译为移动但译为 转移 更为合适 3.1 移动语义 移动语义转移对象的资源控制权 这么直接说定义比较难以理解下面举个例子 这例子源于某网站博主的文章 (文章结尾有指出本文的所有参考文章) 问题一如何将大象放入冰箱
答案是众所周知的。首先你需要有一台特殊的冰箱这台冰箱是为了装下大象而制造的。你打开冰箱门将大象放入冰箱然后关上冰箱门。
问题二如何将大象从一台冰箱转移到另一台冰箱 普通解答打开冰箱门取出大象关上冰箱门打开另一台冰箱门放进大象关上冰箱门。 2B解答在第二个冰箱中启动量子复制系统克隆一只完全相同的大象然后启动高能激光将第一个冰箱内的大象气化消失。 这里的 问题二 就比较好的说明了什么是移动语义。分析一下这个例子 假设现在我们已经有了一个实体大象A需要通过 A 创建另外一个大象B那么我们两套方案
普通解答将 A “移动” 到 B即将 A 的资源 转移给 B2B解答拷贝一份 A 的资源给 B然后再将 A 的资源回收即析构大象A
即便你没有对底层有多了解听了这个例子你也能得出普通解答显然效率更高。
这两个解答就很好地对应了 c 的 2B解答——拷贝构造函数 与 普通解答——移动构造函数。 本文只讲解 拷贝/移动 构造函数不讲解 拷贝/移动 赋值函数 不考虑其他的移动构造函数 的效率比 拷贝构造函数 要高。
因为 拷贝构造函数 会拷贝一份 对象A 的资源需要向操作系统申请资源系统资源是十分昂贵的再将资源赋给 对象B这就降低了性能而 移动构造函数 则是直接将 对象A 的资源转移给 对象B不存在申请资源操作。 既然 移动构造函数 的效率更高那么为什么还保留 拷贝构造函数 需要注意当调用了移动构造函数后A 资源被转移了那么 A 此时相当于是个 “废物” 了如果你在之后仍然使用 A 对象那么会导致未定义行为。
换句话说移动构造函数 相当于进行了 废物利用当明确 对象A 在后续一定不会被使用时那么它的资源可以转给其他需要此类型资源的对象不需要重新申请资源也不用释放资源。 废物也不是完全无用因为它可以 回收利用 但是如果 对象A 你在后续仍然会使用并且需要创建 对象B那么就应该调用 拷贝构造函数。
从形式上看
拷贝构造函数参数为 const 的左值引用移动构造函数参数为 右值引用
上面说了一堆理论下面用代码来实现
A 类如下
class A
{
public:A(int v) :_val{ new int(v) }{ }// * 拷贝构造函数A(const A a) // 使用 const保证了原对象不会改变:_val{ new int(*(a._val)) } // new: 向操作系统申请了资源{ cout 调用了拷贝构造函数 endl; }// * 移动构造函数A(A a) // 采用引用的方式因为需要转移资源:_val{ a._val } // 转移资源{ /*** 由于 a 的资源被转移了* 因此将 a 的资源 _val 指针指向 nullptr* 避免之后误用 a*/a._val nullptr; cout 调用了移动移动函数 endl; }~A() {delete _val;} private:int* _val;
};现在我们来使用 A 类
int main()
{A a(10);A b(a); A c(std::move(a)); // move: 将 a 转为右值引用return 0;
}执行输入为
A 类的构造函数由多个重载函数但是编译器会利用指定的规则取匹配最合适的函数。 比如上述代码中
A b(a); a 的类型为 A此时最合适的函数是 A(const A a);A c(std::move(a)); std::move(a) 的返回值为 A即便 A(const A a); 也能匹配但是最合适的是 A(A a);
看完上面代码相信让你比较疑惑的一点是 std::move 3.2 std::move
源码如下
templatetypename _Tp
constexpr typename std::remove_reference_Tp::type
move(_Tp __t) noexcept
{ return static_casttypename std::remove_reference_Tp::type(__t);
}可能有的地方你看不懂但是不要紧重点在 static_cast 关键字它用于强制类型转换。 如果你对它的源码剖析感兴趣可以见文章 C11的右值引用、移动语义std::move和完美转发std::forward详解 也就是说std::move 的作用 仅仅 是 将一个传入的参数类型强制转换为右值引用(而其返回值为右值引用没有名称因此被视为右值) 我们可以用此函数来 辅助 实现移动语义。 比如上面代码中的 A c(std::move(c)); 当我们确定 a 对象不在使用时同时需要创建 c 对象那么可以 废物利用将 c 转为右值因此调用了 移动构造函数 将 a 的资源转移给 c。 【易错】 std::move 仅仅是做强制类型转换没有实现资源转移的功能。 如果将 A 类的移动构造函数删除那么执行 A c(std::move(a));此时调用的是 拷贝构造函数。 移动构造函数的 资源转移 功能依赖于它自己的内部实现并不是说你调用了 移动构造函数 就实现了 资源转移 。 如果将 A 类的移动构造函数改为 A(A a) { }此时执行 A c(a);即便调用的是拷贝构造函数但是你并没有在内部实现如何转移资源。 资源转移 并不包含 析构对象 在执行完 A c(std::move(a)); 后除非调用析构函数否则 a 对象仍然存在只不过它的资源 (_val) 被转移给 c 对象了由移动构造函数实现。 4. 完美转发std::forward
4.1 引入
在 模板 以及 自动推导类型 中并不是说你指定了 它的类型就是 右值引用
模板Tautoauto
它们既可能是左值引用也可能是右值引用这种引用也被称为 万能引用T 的实际类型需要编译器进行推导。 【注意】 const auto 或者 const T 就是右值引用不需要推导 只有 T 或者 auto 后紧跟 才可能是 万能引用。 template typename T
fun(std::vectorT arg);arg 不是万能引用这是一个 vectorT 类型的右值引用。 在上述提到的情况下只有当 类型需要推导时才是万能引用 对于下面的模板 template typename T
fun(T arg);对于函数调用 funint(100); 来说 arg 不是 万能引用 因为此时已经明确指出 int即明确告诉编译器 T 就是 int所以不存在类型推导所以 arg 不是万能引用 对于函数调用 fun(100); 来说arg 是 万能引用。 因为没有指出 T 的类型所以需要编译器自行推导因此 arg 是万能引用。 当 arg 是 万能引用 时那么 T 的类型是不确定的需要编译器进行推导 当 T 的类型被推导出来时此时它的后面还有 需要进行 引用折叠最终得到参数 arg 的实际类型。 万能引用推导规则 (以 T 为例T 换为 auto 也是一样的)
如果传入的参数是左值那么 T 被推导为 左值引用如果传入的参数是右值那么 T 被推导为 非引用类型
引用折叠规则
T 、T 、T 被折叠为 TT 被折叠为 T
下面看几个例子
模板中的 const T
template typename T
void fun(const T t) { } // t 就是 const 右值引用不需要推导fun(100); // 正确int rr 100;
fun(rr); // 错误rr 视为左值不能给 右值引用初始化模板中的 T
template typename T
void fun2(T t { } int a 0;
int lr a;
int rr 0;// - 以下明确指出了 T 的类型t 不是万能引用
// 但是存在 *引用折叠*
funint (10); // t: int
funint (lr); // t: int - int
funint(10); // t: int - int// - 以下需要推导类型t 是万能引用
fun(0); // 0 为右值所以 T int - t: int
fun(a); // a 为左值所以 T int - t: int - int
fun(rr); // rr 为左值所以 T int - t: int - int// std::move返回值为右值所以 T int - t: int
fun(std::move(rr));auto 自动推导类型
int a 1;
int lr a;
int rr 100;
int fun() { return 0; }const auto rr1 rr; // 正确rr1为右值引用
const auto rr2 lr; // 错误rr2 是右值引用不能用左值初始化// 以下变量类型均为右值引用
auto rr3 100; // 100 为右值所以 auto int - rr3: int
auto rr4 fun(); // 同上// 以下变量类型均为左值引用
auto lr1 lr; // lr 为左值所以 auyo int - lr1: int - int
auto lr2 rr; // 同上 4.2 std::forward
std::forward 主要功能是实现参数转递时既能保留右值属性也能保留左值属性。
这句话可能也很抽象下面来分析一个例子
#include iostream
using namespace std;void print(int) { cout int endl; }
void print(int) { cout int endl; }template typename T
void fun(T t)
{print(t);
}int main()
{int a 0;int lr a;int rr 0;fun(a);fun(0);fun(lr);fun(rr);return 0;
}程序如上在模板函数 fun 中调用了 print 函数并且 传递了参数 t。 下面分析程序的执行结果 fun(a) a 为左值所以 T 被推导为 int那么 print 应输出 int fun(0) 0 为右值所以 T 被推导为 int此时你以为 print 应输出 int然而结果并不是。前面我们提到过 有名称的右值引用 被视为 左值因此虽然 fun 函数的参数 t 为 int但是它被视为 左值所以 print 仍然输出 int
说到这里就不在分析之后两个函数调用结果了留给读者自行分析。
最后程序运行结果为 分析完之后我们发现了关键的问题所在有名称的右值引用 被视为 左值。
如果我们想保留参数 t 的右值属性也就是说如果传递给 fun 函数的参数 t 是右值那么 fun内部调用其他函数时所传递的参数 t 也要保留右值属性即调用 print(int) 函数。
那么怎么实现呢
有一个方案是 std::move假设我们将 fun 函数修改为
template typename T
void fun(T t)
{print(std::move(t));
}显然可以保留 t 的右值属性但是这出现了一个问题如果 t 是左值引用那么 t 也被转为了右值。这不是我们想要的我们更期望 它既能保留右值参数的右值属性也能左值参数的保留左值属性这就是 std::forward 函数的功能
templatetypename _Tp
constexpr _Tp
forward(typename std::remove_reference_Tp::type __t) noexcept
{ return static_cast_Tp(__t);
}templatetypename _Tp
constexpr _Tp
forward(typename std::remove_reference_Tp::type __t) noexcept
{static_assert(!std::is_lvalue_reference_Tp::value,std::forward must not be used to convert an rvalue to an lvalue);return static_cast_Tp(__t);
}看源码可能比较费劲下面我直接说结论 如果你对其源码剖析感兴趣见 C11的右值引用、移动语义std::move和完美转发std::forward详解 第一个函数 参数为左值引用即 _Tp 为左值引用类型返回类型通过 引用折叠 为 左值引用。 第二个函数 参数为右值引用即 _Tp 为右值引用类型返回类型通过 引用折叠 为 右值引用。
因此正确的做法是将 fun 函数改写为
template typename T
void fun(T t)
{ print(std::forwardT(t));
}下面来分析结果应该是什么。
源程序的函数调用顺序如下 fun(a);fun(0);fun(lr);fun(rr);fun(a) a 是左值所以 T 为 int参数 t 为 int此时调用 第一个 forward 函数返回 左值引用所以 print 输出 int fun(0) 0 为右值所以 T 为 int参数 t 为 int此时调用第二个 forward 函数返回 右值引用所以 print 输出 int 其余留给读者去验证尤其是最后一个 最终程序的执行结果为
最后针对最终结果来总结以下
fun(a)、fun(lr)、fun(rr) 的参数都是 左值由于 fun 函数内部 std::forward 函数实现了完美转发传递给 fun 函数的参数为左值那么 std::forward 保留了它的左值属性使得 fun 内部传递给 print 的参数仍然均有 左值属性fun(0) 的参数为 右值由于 fun 函数内部的 std::forward 函数实现了完美转发传递给 fun 函数的参数为右值那么 std::forward 保留了它的右值属性使得 fun 内部传递给 print 的参数仍然具有 右值属性。 本文参考
C Primer 第五版C11的右值引用、移动语义std::move和完美转发std::forward详解详解 C 左值、右值、左值引用以及右值引用c 左值引用与右值引用右值引用大象装冰箱的例子C 新特性 | C 11 | std::forward、万能引用与完美转发
本文如有错误欢迎指正。