每天一遍,右值引用与完美转发

This commit is contained in:
Estom
2021-08-07 14:22:45 +08:00
parent d2feaf32c7
commit 2ed385df51
8 changed files with 268 additions and 7 deletions

View File

@@ -2,10 +2,11 @@
> 参考文献
> * [cnblogs.com/sunchaothu/p/11343517.html](cnblogs.com/sunchaothu/p/11343517.html)
> * [https://www.zhihu.com/question/363686723/answer/1976488046](https://www.zhihu.com/question/363686723/answer/1976488046)
> 为了导入右值和移动语义,首先复习了以下临时对象在函数返回值和传参数时构造了几次;然后对比介绍了左值和右值,以及右值引用的形式和含义。为移动语义和完美转发的介绍做铺垫。
## 1 问题导入
## 1 问题导入——参数与返回值的角度
C++11 引入了 std::move 语义、右值引用、移动构造和完美转发这些特性。
@@ -175,9 +176,104 @@ int && a = 10;
```
* 和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
```
int && a = 10;
a = 100;
cout << a << endl;
````
![](image/2021-07-21-11-45-51.png)
```
![](image/2021-07-21-11-45-51.png)
## 1 问题引入——函数重载的角度
右值引用(Rvalue Reference),与传统的左值引用(Lvalue Reference)相对是绑定到右值的引用。int n;
```
int & lref = n;
int && rref = 1;
```
右值引用是 C++ 11 标准引入的语言特性,理解右值引用,对理解现代 C++ 语义至关重要。而这一概念又经常让人困惑,例如在下面的例子 “overload foo” 中,函数 foo 的调用会匹配哪一个重载void
```
foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
int main() {
int &&rref = 1;
foo(rref); // which overload will be seleted?
}
```
答案是匹配 foo(int&),是不是和想象中的不太一样?
## 2 值类型和值类别
### 值类型和值类别
C++ 中的表达式有两个维度的属性类型type和值类别value category
* 类型这个概念就是我们熟知的类型int, int *, int &, int && 都是类型;其中 int 称为基础类型(fundamental type),后三者称为复合类型(compound type)。
* 值类别分为:左值(lvalue)、亡值(xvalue) 和 纯右值(prvalue),任一个表达式都可以唯一地归类到这三种值类别中的一个,这三种类别称为基础类别(primary category)。除了这三种基础类别外,还有两种混合类别(mixed category):泛左值(glvalue)和右值(rvalue),泛左值包括左值和亡值,右值包括亡值和纯右值。
* 纯右值
* 字面量,
* this指针
* lambda表达式所有内建数值运算表达式a + b, a % b, a & b, a << b&a, 取址表达式...
* 亡值
* 返回类型为对象的右值引用的函数调用或重载运算符表达式,如 std::move(x)
* 转换为对象的右值引用类型的转型表达式,如 static_cast(x)...
* 左值
* 变量、函数、模板形参对象或数据成员的名字,如 std::cin
* 返回类型为左值引用的函数调用或重载运算符表达式,如 std::cout << 1
* ++a/--a*ptr...
* 泛左值
* 包括左值和亡值
* 右值包括亡值和纯右值
### 拥有身份与可以移动
概念
* 拥有身份 (identity):可以确定表达式是否与另一表达式指代同一实体,例如通过比较它们所标识的对象或函数的(直接或间接获得的)地址;
* 可被移动:移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定于这个表达式。
对应
* 拥有身份且不可被移动的表达式被称作左值 (lvalue)表达式;
* 拥有身份且可被移动的表达式被称作亡值 (xvalue)表达式;
* 不拥有身份且可被移动的表达式被称作纯右值 (prvalue)表达式;
* 不拥有身份且不可被移动的表达式无法使用。
* 拥有身份的表达式被称作“泛左值 (glvalue) 表达式”。左值和亡值都是泛左值表达式。
* 可被移动的表达式被称作“右值 (rvalue) 表达式”。纯右值和亡值都是右值表达式。
![](image/2021-08-07-13-48-45.png)
## 3 规则
### 右值引用的规则
* 右值引用同样引入了一条规则:**右值引用类型的变量**只能绑定到**右值表达式**。但奇怪的事情出现了,和左值引用表达式只能是左值不同,**右值引用类型的表达式**既可以是左值,也可以是右值(纯右值或亡值)。
```
void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
int main() {
int &&rref = 1;
foo(rref); // output: lvalue
}
```
> More importantly, when a function has both rvalue reference and lvalue reference overloads, the rvalue reference overload binds to rvalues (including both prvalues and xvalues), while the lvalue reference overload binds to lvalues.
变量 rref 其实是一个左值,它的类型是 int 的右值引用 (int&&)它绑定到一个右值字面量1但它本身是一个左值。一方面因为右值没有名字。另一方面因为右值引用值类型是左值值类别并不冲突。

View File

@@ -102,7 +102,7 @@ class A destruct!
## 2 std::move()
## 3 std::move()
* std::move(lvalue) 的作用就是把一个左值转换为右值。关于左右值的含义我们上一篇博客C++11的右值引用进行过阐述。
@@ -154,6 +154,7 @@ void swap_A(A &a1, A &a2){
说到了 swap, 那就不得不说一下啊 move-and-swap 技术了
## 5 Move and swap 技巧
看下面一段代码,实现了一个 unique_ptr ,和标准的std::unqiue_ptr的含义一致智能指针的一种。
```C++
template<typename T>
@@ -218,3 +219,36 @@ public:
,(事实上仅限右值可以传进来了)。然后 std::swap 负责把原先的资源和source 进行交换,完成了移动赋值。这样写节省了很多代码,很优雅。
## 1 移动语义的应用
右值引用的出现弥补了 C++ 在移动语义上的缺失。在右值引用出现之前我们在函数调用传参时只有两种语义给它一份拷贝按值传递或者给它一份引用按引用传递。void inc_by_value(int i) { ++i; }
```
void inc_by_ref(int &i) { ++i; }
int main() {
int i = 0;
inc_by_value(i);
inc_by_ref(i);
std::cout << i << std::endl; // output: 1
}
```
在上面的这个场景中,语义的缺失并不明显,但当我们处理持有资源的对象时,就不是那么和谐了:
```
class Socket {
public:
void take(Socket other) { sockfd_ = other.sockfd_; }
void take(Socket &other) {
sockfd_ = other.sockfd_;
other.sockfd_ = 0;
}
private:
int sockfd_;
};
```
* 成员函数take的作用是接管外部传入的套接字当我们使用拷贝语义时会使得两个 Socket 对象同时持有同一份资源,可能导致资源的重用;而当使用引用的语义时,我们修改了原对象使其不可用,但并没有将这一点明确告知原对象的使用者,这可能导致资源的误用。
* 在语法上支持移动语义,除了明确告知调用者语义之外,对自动化排错也是有积极意义的,编译器或者其他代码检查工具,可能可以通过语义分析排查亡值对象的错用。
* “return value optimization” 和“copy elision”。所以我认为代码中使用右值引用最重要的目的还是其语义与优化没多大关系。原来就会优化这个东西

View File

@@ -81,7 +81,7 @@ void function(T&& t) {
### 引用折叠规则
* 此模板函数的参数 t 既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题继续解决,即如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参。
* C++ 11标准为了更好地实现完美转发特意为其指定了新的类型匹配规则又称为引用折叠规则假设用 A 表示实际传递参数的类型):
* C++ 11标准为了更好地实现完美转发特意为其指定了新的类型匹配规则又称为引用**折叠规则**(假设用 A 表示实际传递参数的类型):
* 当实参为左值或者左值引用A&)时,函数模板中 T&& 将转变为 A&A& && = A&
* 当实参为右值或者右值引用A&&)时,函数模板中 T&& 将转变为 A&&A&& && = A&&)。
@@ -123,4 +123,124 @@ int main()
// lvalue
```
* 总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。
* 总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。
## 1 问题定义
* 完美转发就是将函数实参以其原本的值类别转发出去。**转发值类别**
```
void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename /*T*/> void bar(/*T*/ x) { /*call foo with x*/ }
int main() {
int i;
bar(i); // expecting output: lvalue
bar(1); // expecting output: rvalue
}
```
* 在这里,变量 i 经历了两次转发,所以我们需要先后解决这两次转发的值类别问题。
* 用户调用 bar 时,参数的值类别问题当用户以左值表达式调用 bar 时,确保其实例化(Instantiation)的形参类型为左值引用当
* 用户以右值表达式调用 bar 时,确保其实例化的形参类型为右值引用。
* bar 调用 foo 时,参数的值类别问题
* 当 bar 的形参类型为左值引用时,将其以左值转发给 foo
* 当 bar 的形参类型为右值引用时,将其以右值转发给 foo
> C++ 通过转发引用来解决第一个匹配,通过 std::forward 来解决第二个匹配。
## 2 转发引用
### 原理
* 转发引用基于一个叫做引用坍缩(Reference Collapsing)的原理:
> rvalue reference to rvalue reference collapses to rvalue reference, all other combinations form lvalue reference.typedef int& lref;
```
typedef int&& rref;
int n;
lref& r1 = n; // type of r1 is int& , int& + & => int &
lref&& r2 = n; // type of r2 is int& , int& + && => int &
rref& r3 = n; // type of r3 is int& , int&& + & => int &
rref&& r4 = 1; // type of r4 is int&&, int&& + && => int &&
```
### 举例说明
```C++
void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename T> int bar(T &&x); // x is a forwarding reference
int main() {
int i = 1;
int &lref = i;
int &&rref = 1;
bar(1); // T 为 int, decltype(x) 为 int&&
bar(i); // T 为 int&, decltype(x) 为 int&
bar(lref); // T 为 int&, decltype(x) 为 int&
bar(rref); // T 为 int&, decltype(x) 为 int&
bar(std::move(rref)); // T 为 int, decltype(x) 为 int&&
bar<int &&>(1); // T 为 int&&, decltype(x) 为 int&&
}
```
### 第一步转发问题解决
* 解决了调用 bar 时参数值类别的问题,现在我们将 bar 的参数传递给
```C++
foovoid foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename T> int bar(T &&x) { foo(x); }
```
## 3 forward转发
### forward原理
* std::forward 的实现如下:
```C++
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);
}
```
* 实现了将左值转发为左值或右值,将右值转发为右值。
### 举例说明
```C++
void foo(const int &) { std::cout << "lvalue" << std::endl; }
void foo(const int &&) { std::cout << "rvalue" << std::endl; }
int main() {
int i = 1;
foo(std::forward<int>(i)); // output: rvalue; forward lvalue -> rvalue
foo(std::forward<int&>(i)); // output: lvalue; forward lvalue -> lvalue
foo(std::forward<int&&>(i)); // output: rvalue; forward lvalue -> rvalue
foo(std::forward<int>(1)); // output: rvalue; forward rvalue -> rvalue
foo(std::forward<int&>(1)); // error: static_assert failed due to requirement '!is_lvalue_reference<int &>::value' "can not forward an rvalue as an lvalue"
foo(std::forward<int&&>(1)); // output: rvalue; forward rvalue -> rvalue
}
```
### 第二步转发的实现
```
void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename T> int bar(T &&x) { foo(std::forward<T>(x)); }
int main() {
int i = 1;
bar(i); // output: lvalue
bar(1); // output: rvalue
}
```
* 我们总结一下。完美转发问题是将函数的参数以其原本值类别转发出去的问题。转发引用 和 std::forward 共同解决了完美转发问题。其中转发引用将函数的左值实参推导为左值引用类型右值实参推导为右值引用类型。std::forward 将左值引用类型的实参转发为左值,将右值引用类型的实参转发为右值。

View File

@@ -0,0 +1,2 @@
> 主要用来记录 effectiveC++ 和more effective c++中涉及到的基础知识和高级特性。
> 还有modern C++中的内容。主要包括C++11 17 20中新添加的特性。

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1 +0,0 @@
> 主要用来记录 effectiveC++ 和more effective c++中涉及到的基础知识和高级特性。

View File

@@ -1,9 +1,19 @@
## Introduction
### Background
* 恶意软件
* 恶意软件检测的主要方法
* 恶意软件检测的主要问题(隐私问题)
### Problems
* 联邦学习应用的问题(非独立同分布)
* 什么是非独立同分布
* 非独立同分布的数学表示方法
* 非独立同分布对机器学习和联邦学习产生的影响,以及数学上的证明。
### Contributions
> what we do to achieve aims.
* fedRed算法