如果你还不知道 C++ 11 引入的右值引用是什么,可以读读这篇文章,看看有什么 启发;如果你已经对右值引用了如指掌,也可以读读这篇文章,看看有什么 补充。欢迎交流~
尽管 C++ 17 标准 在去年底已经正式发布了,但由于 C++ 语言变得 越来越复杂,让许多人对很多新特性 望而却步。
对于 2011 年发布的 C++ 11 标准,很多人虽然对 右值引用/移动语义/拷贝省略/通用引用/完美转发 之类的概念都 有所耳闻,却没有详细了解其 设计初衷/实现原理,甚至对一些细节 有所误解(包括我 )。
我刚开始学习 C++ 的时候,也常常 混淆 这几个 概念。但随着深入了解、与人探讨,才逐步理清楚这几个概念的来龙去脉。先分享几个我曾经犯过的错误。
ES.56: Write
std::move()
only when you need to explicitly move an object to another scope
std::string base_url = tag->GetBaseUrl();
if (!base_url.empty()) {
UpdateQueryUrl(std::move(base_url) + "&q=" + word_);
}
LOG(INFO) << base_url; // |base_url| may be moved-from
上述代码的问题在于:使用 std::move()
移动局部变量 base_url
,会导致后续代码不能使用该变量;如果使用,会出现 未定义行为 (undefined behavior)(参考:std::basic_string(basic_string&&)
)。
如何检查 移动后使用 (use after move):
C.64: A move operation should move and leave its source in a valid state
auto p = std::make_unique<int>(1);
auto q = std::move(p);
assert(p == nullptr); // OK: use after move
p.reset(new int{2}); // or p = std::make_unique<int>(2);
assert(*p == 1); // OK: realive now
很多人认为:被移动的值会进入一个 非法状态 (invalid state),对应的内存不能再访问。
其实,C++ 标准要求对象 遵守 § 3 移动语义:被移动的对象进入一个 合法但未指定状态 (valid but unspecified state),调用该对象的方法不会出现异常。要求处于这个状态的对象:
std::unique_ptr::~unique_ptr()
检查指针是否需要 delete
)std::unique_ptr
恢复为 nullptr
)另外,基本类型的值(例如 int/double
)的移动和拷贝相同。例如,int i = 42;
被移动后,保持为原有值 i == 42
。
F.48: Don’t
return std::move(local)
std::unique_ptr<int> foo() {
auto ret = std::make_unique<int>(1);
//...
return std::move(ret); // -> return ret;
}
上述代码的问题在于:没必要使用 std::move()
移动返回值。
C++ 会把 即将离开作用域的 返回值 当成 右值(参考 § 2.1),对返回的对象进行 § 3 移动构造(语言标准);如果编译器允许 § 4 拷贝省略,还可以省略这一步的构造,直接把 ret
存放到返回值的内存里(编译器优化)。
Never apply
std::move()
orstd::forward()
to local objects if they would otherwise be eligible for the return value optimization. —— Meyer Scott, Effective Modern C++
另外,误用 std::move()
会 阻止 编译器的拷贝省略 优化。不过聪明的 Clang 会提示 -Wpessimizing-move
警告。
F.18: For “will-move-from” parameters, pass by
X&&
andstd::move()
the parameter
std::unique_ptr<int> bar(std::unique_ptr<int>&& val) {
//...
return val; // not compile
// -> return std::move/forward(val);
}
上述代码的问题在于:没有对返回值使用 std::move()
(编译器提示 std::unique_ptr(const std::unique_ptr&) = delete
错误)。
If-it-has-a-name Rule:
- Named rvalue references are lvalues.
- Unnamed rvalue references are rvalues.
因为不论 左值引用 还是 右值引用 的变量(或参数)在初始化后,都是左值(参考 § 2.1):
所以,返回右值引用变量时,需要使用 std::move()
/std::forward()
显式的 § 5.4 移动转发 或 § 5.3 完美转发,将变量的类型 “还原” 为右值引用。
C.20: If you can avoid defining default operations, do
C.21: If you define or
=delete
any default operation, define or=delete
them allC.80: Use
=default
if you have to be explicit about using the default semanticsC.66: Make move operations
noexcept
实际上,多数情况下:
=default
让编译器生成 移动构造/赋值函数,否则 容易写错noexcept
不抛出异常(编译器生成的版本会自动添加),否则 不能高效 使用标准库和语言工具例如,标准库容器 std::vector
在扩容时,会通过 std::vector::reserve()
重新分配空间,并转移已有元素。如果扩容失败,std::vector
满足 强异常保证 (strong exception guarantee),可以回滚到失败前的状态。
为此,std::vector
使用 std::move_if_noexcept()
进行元素的转移操作:
noexcept
移动构造函数(高效;不抛出异常)noexcept
移动构造函数(高效;如果异常,无法回滚)如果 没有定义移动构造函数 或 自定义的移动构造函数没有 noexcept
,会导致 std::vector
扩容时执行无用的拷贝,不易发现。
之所以会出现上边的误解,往往是因为 C++ 语言的复杂性 和 使用者对基础知识的掌握程度 不匹配。
划重点 —— 值 (value) 和 变量 (variable) 是两个独立的概念:
i + j + k
)值类别 (value category) 可以分为两种:
C++ 17 细化了
prvalue/xvalue/lvalue
和rvalue/glvalue
类别,本文不详细讨论。
引用类型 (reference type) 属于一种 变量类型 (variable type),将在 § 2.2 详细讨论。
在变量 初始化 (initialization) 时,需要将 初始值 (initial value) 绑定到变量上;但 引用类型变量 的初始化 和其他变量不同:
引用类型 可以分为两种:
&
符号引用 左值(但不能引用右值)&&
符号引用 右值(也可以移动左值)void f(Data& data); // 1, data is l-ref
void f(Data&& data); // 2, data is r-ref
Data data;
Data& data1 = data; // OK
Data& data1 = Data{}; // not compile: invalid binding
Data&& data2 = Data{}; // OK
Data&& data2 = data; // not compile: invalid binding
Data&& data2 = std::move(data); // OK
f(data); // 1, data is lvalue
f(Data{}); // 2, data is rvalue
f(data1); // 1, data1 is l-ref type and lvalue
f(data2); // 1, data2 is r-ref type but lvalue
左值引用 变量 data1
在初始化时,不能绑定右值 Data{}
右值引用 变量 data2
在初始化时,不能绑定左值 data
但可以通过 std::move()
将左值 转为右值引用(参考 § 5.4)
右值引用 变量 data2
被初始化后,在作用域内是 左值(参考 § 1.4),所以匹配 f()
的 重载 2
另外,C++ 还支持了 常引用 (c-ref, const reference),同时接受 左值/右值 进行初始化:
void g(const Data& data); // data is c-ref
g(data); // ok, data is lvalue
g(Data{}); // ok, data is rvalue
常引用和右值引用 都能接受右值的绑定,有什么区别呢?
const Data& data1 = Data{}; // OK: extend lifetime
data1.modify(); // not compile: const
Data&& data2 = Data{}; // OK: extend lifetime
data2.modify(); // OK: non-const
void f(const Data& data); // 1, data is c-ref
void f(Data&& data); // 2, data is r-ref
f(Data{}); // 2, prefer 2 over 1 for rvalue
引用折叠 (reference collapsing) 是 std::move()
/std::forward()
的实现基础:
using Lref = Data&;
using Rref = Data&&;
Data data;
Lref& r1 = data; // r1 is Data&
Lref&& r2 = data; // r2 is Data&
Rref& r3 = data; // r3 is Data&
Rref&& r4 = Data{}; // r4 is Data&&
在 C++ 11 强化左右值概念后,我们可以针对右值进行优化。于是,C++ 11 中就提出了 移动语义 (move semantic):
由于基本类型不包含资源,其移动和拷贝相同:被移动后,保持为原有值。
针对包含了资源的对象,我们可以通过移动对象的资源进行优化。例如,常用的 STL 类模板都有:
template<typename T>
class vector {
public:
vector(const vector& rhs); // copy data
vector(vector&& rhs) noexcept; // move data
~vector(); // dtor
private:
T* data_ = nullptr;
size_t size_ = 0;
};
vector::vector(const vector& rhs) : data_(new T[rhs.size_]) {
auto &lhs = *this;
lhs.size_ = rhs.size_;
std::copy_n(rhs.data_, rhs.size_, lhs.data_); // copy data
}
vector::vector(vector&& rhs) noexcept {
auto &lhs = *this;
lhs.size_ = rhs.size_;
lhs.data_ = rhs.data_; // move data
rhs.size_ = 0;
rhs.data_ = nullptr; // set data of rhs to null
}
vector::~vector() {
if (data_) // release only if owned
delete[] data_;
}
上述代码中,构造函数 vector::vector()
根据参数的左右值类型判断:
new[]
/std::copy_n
拷贝原对象的内存(本方案有一次冗余的默认构造,可优化)data_
、内存大小 size_
复制到新对象,并把原对象这两个成员置 0
析构函数 vector::~vector()
检查 data_ 是否有效,决定是否需要释放资源。
除了能判断参数是否为左右值,成员函数还可以判断当前对象的左右值类型:给成员函数加上 引用限定符 (reference qualifier),针对对象本身的左右值类型进行优化。
class Foo {
public:
Data& data() & { return data_; } // lvalue, l-ref
Data data() && { return move(data_); } // rvalue, move-out
};
auto ret1 = foo.data(); // foo is lvalue, copy
auto ret2 = Foo{}.data(); // foo is rvalue, move
在之前写的 资源管理小记 提到:如果资源是不可复制的,那么装载资源的对象也应该是不可复制的。
如果资源对象不可复制,那么只能通过移动,创建新对象。例如,智能指针 std::unique_ptr
只允许移动构造,不允许拷贝构造。
template<typename T>
class unique_ptr {
public:
unique_ptr(const unique_ptr& rhs) = delete;
unique_ptr(unique_ptr&& rhs) noexcept; // move only
private:
T* data_ = nullptr;
};
unique_ptr::unique_ptr(unique_ptr&& rhs) noexcept {
auto &lhs = *this;
lhs.data_ = rhs.data_;
rhs.data_ = nullptr;
}
上述代码中,unique_ptr
的移动构造过程和 vector
类似:
data_
复制到新对象data_
置为空移动语义只是语言上的一个 概念,具体是否移动对象的资源、如何移动对象的资源,都需要通过编写代码 实现。而移动语义常常被 误认为,编译器 自动生成 移动对象本身的代码(§ 4 拷贝省略)。
为了证明这一点,我们可以实现不遵守移动语义的 bad_vec::bad_vec(bad_vec&& rhs)
,执行拷贝语义:
bad_vec::bad_vec(bad_vec&& rhs) : data_(new T[rhs.size_]) {
auto &lhs = *this;
lhs.size_ = rhs.size_;
std::copy_n(rhs.data_, rhs.size_, lhs.data_); // copy data
}
那么,一个 bad_vec
对象在被 move
移动后仍然可用:
bad_vec<int> v_old { 0, 1, 2, 3 };
auto v_new = std::move(v_old);
v_old[0] = v_new[3]; // ok, but odd :-)
assert(v_old[0] != v_new[0]);
assert(v_old[0] == v_new[3]);
虽然代码可以那么写,但是在语义上有问题:进行了拷贝操作,违背了移动语义的初衷。
尽管 C++ 引入了移动语义,移动的过程 仍有优化的空间 —— 与其调用一次 没有意义的移动构造函数,不如让编译器 直接跳过这个过程 —— 于是就有了 拷贝省略 (copy elision)。
然而,很多人会把移动语义和拷贝省略 混淆:
ret
—— 在 不同作用域 里,共享 同一块内存)C++ 17 要求编译器对 纯右值 (prvalue, pure rvalue) 进行拷贝省略优化。(参考)
Data f() {
Data val;
// ...
throw val;
// ...
return val;
// NRVO from lvalue to ret (not guaranteed)
// if NRVO is disabled, move ctor is called
}
void g(Date arg);
Data v = f(); // copy elision from prvalue (C++ 17)
g(f()); // copy elision from prvalue (C++ 17)
初始化 局部变量、函数参数时,传入的纯右值可以确保被优化 —— Return Value Optimization (RVO);而返回的 将亡值 (xvalue, eXpiring value) 不保证被优化 —— Named Return Value Optimization (NRVO)。
揭示
std::move()
/std::forward()
的原理,需要读者有一定的 模板编程基础。
C++ 11 引入了变长模板的概念,允许向模板参数里传入不同类型的不定长引用参数。由于每个类型可能是左值引用或右值引用,针对所有可能的左右值引用组合,特化所有模板 是 不现实的。
假设没有 通用引用的概念,模板 std::make_unique<>
至少需要两个重载:
template<typename T, typename... Args>
unique_ptr<T> make_unique(const Args&... args) {
return unique_ptr<T> {
new T { args... }
};
}
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T> {
new T { std::move<Args>(args)... }
};
}
const Args&... args
,只要展开 args...
就可以转发这一组左值引用Args&&... args
,需要通过 std::move()
转发出去,即 std::move<Args>(args)...
(为什么要转发:参考 § 1.4)上述代码的问题在于:如果传入的 args
既有 左值引用 又有 右值引用,那么这两个模板都 无法匹配。
Item 24: Distinguish universal references from rvalue references. —— Meyer Scott, Effective Modern C++
Meyer Scott 指出:有时候符号 &&
并不一定代表右值引用,它也可能是左值引用 —— 如果一个引用符号需要通过 左右值类型推导(模板参数类型 或 auto
推导),那么这个符号可能是左值引用或右值引用 —— 这叫做 通用引用 (universal reference)。
// rvalue ref: no type deduction
void f1(Widget&& param1);
Widget&& var1 = Widget();
template<typename T> void f2(vector<T>&& param2);
// universal ref: type deduction
auto&& var2 = var1;
template<typename T> void f3(T&& param);
上述代码中,前三个 &&
符号不涉及引用符号的左右值类型推导,都是右值引用;而后两个 &&
符号会 根据初始值推导左右值类型:
对于 var2
因为 var1
是左值,所以 var2
也是左值引用
推导不会参考 var1
的变量类型
对于 T&&
如果 param
传入左值,T&&
是左值引用 std::remove_reference_t<T>&
如果 param
传入右值,T&&
是右值引用 std::remove_reference_t<T>&&
基于通用引用,§ 5.1 的模板 std::make_unique<>
只需要一个重载:
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T> {
new T { std::forward<Args>(args)... }
};
}
其中,std::forward()
实现了 针对不同左右值类型的转发 —— 完美转发。
什么是 完美转发 (perfect forwarding):
因此,std::forward()
定义两个 不涉及 左右值类型 推导 的模板(不能使用 通用引用参数):
template <typename T>
T&& forward(std::remove_reference_t<T>& val) noexcept {
// forward lvalue as either lvalue or rvalue
return static_cast<T&&>(val);
}
template <typename T>
T&& forward(std::remove_reference_t<T>&& val) noexcept {
// forward rvalue as rvalue (not lvalue)
static_assert(!std::is_lvalue_reference_v<T>,
"Cannot forward rvalue as lvalue.");
return static_cast<T&&>(val);
}
实参/返回值 类型 | 左值引用返回值 | 右值引用返回值 |
---|---|---|
(重载 1)左值引用实参 | 完美转发 | 移动转发 |
(重载 2)右值引用实参 | 语义错误 | 完美转发 |
T
的类型无关static_cast<T&&>(val)
通过 § 2.3 引用折叠 实现 完美转发 或 移动转发,和实参类型无关类似的,std::move()
只转发为右值引用类型:
template <typename T>
std::remove_reference_t<T>&& move(T&& val) noexcept {
// forward either lvalue or rvalue as rvalue
return static_cast<std::remove_reference_t<T>&&>(val);
}
实参/返回值 类型 | 右值引用返回值 |
---|---|
左值引用实参 | 移动转发 |
右值引用实参 | 移动转发/完美转发 |
T&& val
(无需两个模板,使用时无需指出 T
的类型)static_cast<std::remove_reference_t<T>&&>(val)
直接转为右值引用类型std::move<T>()
等价于 std::forward<std::remove_reference_t<T>&&>()
虽然这些东西你不知道,也不会伤害你;但如果你知道了,就可以合理利用,从而提升开发效率,避免不必要的问题。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/9kfTHrQR2roVbTLE9r6rsw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。