C++的引用与重载解析
本文讲述了C++的左右值和左右值和左右值引用,以及它们在重载解析当中的顺序。
左右值和左右值引用和move
左右值之分
expression
| \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
广义左值glvalue,都是可以有标识符的。广义右值rvalue,都是可移动(std::move)的。xvalue属于这两者,既可以移动又可以有标识符。
左值lvalue: 可以取地址,有名字的,非临时的就是左值。non-const左值可以放在“=”的左侧。
-
变量int a的a,引用int& a的a,数组元素int arr[]的arr[i],左值对象的成员
-
返回左值引用的表达式/函数,例如,++x,x=1,函数返回值如下
#include <iostream> using namespace std; int& square(int& number) { number = number * number; // 修改参数 return number; // 返回左值引用 } int main() { int number = 5; cout << "number: " << number << endl; // 输出:number: 5 square(number) = 25; // 可以在左侧赋值 cout << "number: " << number << endl; // 输出:number: 25 return 0; } -
特殊:字符串字面量,如"hello world!",但是字符串字面量是const类型不可修改
纯右值prvalue: 不能取地址,没有名字的,临时的就是右值。不可以放在“=”的左侧。
- 除字符串字面量外的字面量,如true
- 返回非引用的表达式/函数,例如函数返回了一个类
- 临时对象
- lambda表达式
将亡值xvalue,Expiring Value:不可以放在“=”的左侧。
-
将要被移动的对象
std::string s1 = "hello"; std::string s2 = std::move(s1); // 使用std::move()将s1移动到s2中,此时s1就成为了一个xvalue。 -
强制类型转换表达式(static_cast、dynamic_cast、reinterpret_cast、const_cast)的结果,其中源类型和目标类型都是右值引用类型,并且在此过程中发生了一次类型转换
class Base { /*...*/ }; class Derived : public Base { /*...*/ }; Derived d; Base&& b = static_cast<Base&&>(d); // 在进行类型转换时,d被转换为一个右值引用类型的Base对象,并且在此过程中发生了一次类型转换,因此b成为了一个xvalue。 -
函数返回值为右值引用类型
std::string&& foo() { return std::string{"world"}; // 返回一个右值引用,由于返回的对象是临时创建的,因此它将在函数返回后被销毁,因此该返回值是一个xvalue。 } std::string s = foo(); // 调用foo()函数,将右值引用赋值给s,此时s成为了一个xvalue。 -
右值引用类型的变量
std::string&& s = std::string{"world"}; // 在声明变量时,将一个右值引用绑定到一个临时创建的std::string对象上,此时s成为了一个xvalue。
左值引用: 相当于指针指向某个左值,取了左值的地址。就是说左值引用要求右边的值可以取地址,若不能取地址可以像这样const int& a = 1;,这样做实际是创建临时变量。
右值引用: 相当于指针指向某个右值,右值没有地址意味着创建了临时变量来存储。类似于左值引用使用const,但是使用了const无法修改,这里可以修改。
为何会有左右值?引用实际上就是简化了指针,或者说内存标识符。为了精确控制内存生命周期,避免没必要的内存拷贝。
std::move
std::move本质是什么?std::move是将对象转换成右值,仅仅是“标记”一下,本身并没有做其他任何操作。标记为右值以便在将来的移动赋值和移动构造中 使用不同于拷贝赋值和拷贝构造的函数,结合函数重载实现了深拷贝和浅拷贝的功能。
误解:std::move操作改变了资源的所有权
std::move仅仅是转换为右值,并没有改变其他东西。“改变所有权”这个操作是在移动赋值和移动构造里实现的,具体怎么改变所有权归移动赋值和移动构造所解释。以下介绍“改变所有权”这个操作:
class C {
public:
int a{};
float b{};
char c{};
std::string s;
int *p{};
size_t size{};
C(C &&other) noexcept {
//以下三个POD类型 都提示使用std::move无效
a = std::move(other.a); //Clang-Tidy: Std::move of the expression of the trivially-copyable type 'int' has no effect;
b = std::move(other.b);
c = std::move(other.c);
//非POD move
s = std::move(other.s);
//指针对于移动语义 可以直接拿过来 避免拷贝
p = other.p;
size = other.size;
other.p = nullptr;
other.size = 0;
}
};
因此,对于类似std::vector扩容时调用元素的移动构造函数,实际上就是想实现一个资源所有权转移。这里说成“资源所有权转移”其实是有歧义的,好像是直接改变指向,没有进行任何赋值一样。实现上如上代码所示:
对于POD类型,直接赋值;对于Non-Trivial类型,继续调用move,有一种递归调用的感觉,最终还是POD类型;对于指针,则是直接拿来用。
引用坍缩规则(Reference Collapsing)
左右值引用的本身仍然是左值。
因此在函数传参、模板等情况。会遭遇引用组合的问题。
| 声明 | 传入 | 内部解析为 |
|---|---|---|
| T& | & | T& |
| T& | && | T& |
| T&&(转发引用) | & | T& |
| T&&(转发引用) | && | T&& |
转发引用,在声明时,可以把引用性完整传递过去,在模板中是有用的,可以不损失引用性。
std::forward<T>(rlValue),在调用函数时,可以把引用性完整传递过去。
template<typename T>
void func(T &&t) {
foo(std::forward<T>(t));
}
rvalue的生命期延长规则
一个prvalue被绑定到一个引用上,则生命周期会被延长到这个引用的生命周期。xvalue生命周期不会被这样延长,语句结束即结束。
class MyClass {
public:
std::string s;
};
int main() {
MyClass &&m = std::move(MyClass());
m.s = "hello";
/*
* 对于一个右值引用,我们无法确定它指向的对象是否是持久的。
* 临时对象 MyClass() 是在表达式中创建的,它的生命周期只存在于这个表达式中,一旦表达式结束,它就会被销毁,
* 因此绑定到这个对象上的右值引用也就变得无效了。
* 此时的赋值是未定义行为。
*/
return 0;
}
重载解析
重载的重要机制:函数名称修饰
函数的名字修饰(Decorated Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。 多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。
重载的区分点
函数重载的函数签名要求:不同个数形参,或形参类型不同,返回值不要求(因为在入口时需要知道使用哪个函数而不是结束时知道)。
注意:
- 在类成员函数中:函数类型的const和非const(后置const)可以作为区分点
- 引用和非引用不可作为区分点,但是const引用和非const引用是可以作为区分的
代码示例如下:
定义部分
#include <iostream>
template<typename T>
class C {
public:
//因为编译器MSVC,g++,clang++对 T和T的任何引用(不论是否为const成员函数) 都是视作相同的, 导致冲突, 会造成ambiguous call error, 这里对它注释掉
//void func(T a) {
//std::cout << "T a" << std::endl;
//}
//不同类型可以作为重载依据, 假设不同于T的类型为float
void func(float a) {
std::cout << "float a" << std::endl;
}
void func(T a, T b) {
std::cout << "T a, T b" << std::endl;
}
//出错, 返回值不可作为依据
//T func(T a) {};
/************引用相关****************/
void func(T &a) {
std::cout << "T& a" << std::endl;
}
void func(T *a) {
std::cout << "T& a" << std::endl;
}
void func(T &&a) {
std::cout << "T&& a" << std::endl;
}
/************const相关****************/
// const函数可以作为重载依据
void func(T a) const {
std::cout << "T a, func const" << std::endl;
}
// const+值传递不能作为重载依据
//void func(const T a) {}
// const+引用可以作为重载依据
void func(const T &a) {
std::cout << "const T &a" << std::endl;
}
void func(const T &&a) {
std::cout << "const T &a" << std::endl;
}
};
class Trivial {
};
class NonTrivial {
std::string s;
};
template<typename T>
T &&createRR() {
return std::move(T());
}
template<typename T>
T create() {
return T();
}
实验部分
void constAndNonConstFuncInvoke() {
const C<Trivial> c;
Trivial t;
// 因为const成员函数仅仅有1个, 所以不会出现ambiguous call error, 倘若有多个const成员函数, 则会出现ambiguous call error
c.func(t); //T a, func const
}
void trivialTest() {
C<Trivial> c;
Trivial a = Trivial();
Trivial b = Trivial();
const Trivial ca1 = Trivial();
Trivial const ca2 = Trivial();
Trivial &ra = a;
Trivial *pa = &a;
c.func(a); //T& a
c.func(ra); //T& a
c.func(pa); //T& a
c.func(std::forward<Trivial>(a)); //T&& a
c.func(std::move(b)); //T&& a
c.func(create<Trivial>()); //T&& a
c.func(createRR<Trivial>()); //T&& a
c.func(ca1); //const T &a
c.func(ca2); //const T &a
}
void nonTrivialTest() {
C<NonTrivial> c;
NonTrivial a = NonTrivial();
NonTrivial b = NonTrivial();
const NonTrivial ca1 = NonTrivial();
NonTrivial const ca2 = NonTrivial();
NonTrivial &ra = a;
NonTrivial *pa = &a;
c.func(a); //T& a
c.func(ra); //T& a
c.func(pa); //T& a
c.func(std::forward<NonTrivial>(a)); //T&& a
c.func(std::move(b)); //T&& a
c.func(create<NonTrivial>()); //T&& a
c.func(createRR<NonTrivial>()); //T&& a
c.func(ca1); //const T &a
c.func(ca2); //const T &a
}