C++里面的左值,右值
C++里面有区别左值、右值的需求,比如左值可以调用拷贝构造函数,而右值会调用移动构造函数。示例如下:
Demo
#include <iostream>
#include <utility> // std::move
class MyClass {
public:
int value;
// 普通构造函数
MyClass(int v) : value(v) {
std::cout << "Constructor: value = " << value << std::endl;
}
// 拷贝构造函数
MyClass(const MyClass& other) : value(other.value) {
std::cout << "Copy Constructor: value = " << value << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : value(other.value) {
std::cout << "Move Constructor: value = " << value << std::endl;
other.value = 0; // 清理被移动对象
}
// 析构函数
~MyClass() {
std::cout << "Destructor: value = " << value << std::endl;
}
};
int main() {
std::cout << "=== 左值调用拷贝构造 ===" << std::endl;
MyClass a(10); // 调用普通构造
MyClass b(a); // a 是左值,调用拷贝构造
std::cout << "\n=== 使用 std::move 将左值转为右值 ===" << std::endl;
MyClass d(std::move(a)); // a 是左值,但 std::move 转为右值,调用移动构造
return 0;
}
输出结果如下:
[porco@poyun183010267861 MyTinySTL]$ ./code
=== 左值调用拷贝构造 ===
Constructor: value = 10
Copy Constructor: value = 10
=== 使用 std::move 将左值转为右值 ===
Move Constructor: value = 10
Destructor: value = 10
Destructor: value = 10
Destructor: value = 0
C++中模板函数里面同样也有区分左值和右值的需求。但是,C++里面有命名的变量是左值,这就会带来参数在传递的过程中,左值、右值属性丢失的问题。
比如下面这个例子,我在模板函数func里面构造一个MyClass obj对象,我原意是希望,如果param是左值,那么调用拷贝构造函数,如果param是右值,那么调用移动构造函数。
但实际看输出,我们可以发现,调用的都是拷贝构造函数。这就是因为param是一个命名参数,命名参数是可以取地址的,就是说param作为函数的入参,是有分配地址的,所以它就是一个左值类型,所以它就会调用拷贝构造函数。
Demo
#include <iostream>
#include <utility> // std::move, std::forward
class MyClass {
public:
MyClass() {}
MyClass(const MyClass&) {
std::cout << "Copy Constructor\n";
}
MyClass(MyClass&&) noexcept {
std::cout << "Move Constructor\n";
}
};
// 一个普通的模板函数,参数是 T
template <typename T>
void func(T&& param) {
std::cout << "func(T param) called\n";
MyClass obj(param); // 注意:这里 param 是有名字的变量,一定是左值
}
int main() {
MyClass a;
std::cout << "=== 命名变量(左值) ===\n";
func(a); // 左值 → 拷贝构造
std::cout << "=== 命名变量 + std::move 转为右值 ===\n";
func(std::move(a)); // 传右值,但 param 是左值 → 拷贝构造
return 0;
}
那么我想要实现保留模板参数的左值、右值属性,我该怎么办?这就是C++11引入的完美转发。
C++实现完美转发
我们首先将上面的func函数改造一下
// 一个普通的模板函数,参数是 T
template <typename T>
void func(T&& param) {
std::cout << "func(T param) called\n";
MyClass obj(std::forward<T>(param)); // 注意:这里 param 是有名字的变量,一定是左值
}
现在的输出就会发生变化了,它就会和我们预期的一样,对于右值调用移动构造函数:
[porco@poyun183010267861 MyTinySTL]$ ./code
=== 命名变量(左值) ===
func(T param) called
Copy Constructor
=== 命名变量 + std::move 转为右值 ===
func(T param) called
Move Constructor
那么完美转发在C++里面是如何实现的呢,是通过一个叫做引用折叠的规则。先说C++中引用折叠的规则如下:
& & -> &
& && -> &
&& & -> &
&& && -> &&
总结就是只要有一个&,结果就是&,只有两个&&,结果才是&&。这要怎么理解呢?
我们先给出forward的代码实现:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) {
return static_cast<T&&>(arg);
}
再来看我们的func函数:
// 一个普通的模板函数,参数是 T
template <typename T>
void func(T&& param) {
std::cout << "func(T param) called\n";
MyClass obj(forward<T>(param)); // 注意:这里 param 是有名字的变量,一定是左值
}
第一种情况:func(a);
这时候a是左值,a的类型是int &。但是func函数的入参是T&&,那么T&&怎么接受一个int &类型的入参呢?答案是将T推导为int &,这样T&&类型就变成了int & &&,根据引用折叠规则,int & &&就相当于int &,这样就可以接受一个int &的左值参数了。所以此时T等价于 int&。
那么此时forward函数的实现将变成:
int& && forward(int& arg) {
return static_cast<int& &&>(arg);
}
根据引用折叠规则,它实际等价于
int& forward(int& arg) {
return static_cast<int&>(arg);
}
所以相当于左值变成左值,保留了左值的属性。接下来我们看如何保留右值的属性。
第二种情况:func(std::move(a));
这时候a是右值,a的类型是int &&。但是func函数的入参是T&&,那么T&&怎么接受一个int &&类型的入参呢?答案是将T推导为int &&,这样T&&就变成了int&& &&,根据引用折叠规则,就等价于int &&,此时T等价于int&&。
但是这里有个问题,就是forward实际只能接受左值类型的参数,因为std::remove_reference<T>::type&会把T的引用属性去掉,int&&变成int,后面接上一个&,最终变成int&,所以forward函数只能接受一个int&类型。不过int&的意思就是左值,而param是命名变量,所以就是左值,所以也能接受。
forward函数的实现变成了
int&& forward(int& arg) {
return static_cast<int&&>(arg);
}
标准的C++完美转发实现
按照C++11标准实现的如下:
template <typename T>
inline constexpr T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
return static_cast<T&&>(arg);
}
template <typename T>
inline constexpr T&& forward(typename std::remove_reference<T>::type&& arg) noexcept {
static_assert(!std::is_lvalue_reference<T>::value,
"bad forward of rvalue as lvalue");
return static_cast<T&&>(arg);
}
可以看到,标准的写法里面,定义了两个版本,一个是左值的版本,一个是右值的版本。为什么要定义两个版本呢?我们的func函数只用到了左值的版本。运行下面的代码:
Demo
#include <iostream>
#include <utility> // std::move, std::forward
#include <type_traits>
class MyClass {
public:
MyClass() {}
MyClass(const MyClass&) {
std::cout << "Copy Constructor\n";
}
MyClass(MyClass&&) noexcept {
std::cout << "Move Constructor\n";
}
};
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
std::cout << "left" << std::endl;
return static_cast<T&&>(arg);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& arg) noexcept {
std::cout << "right" << std::endl;
static_assert(!std::is_lvalue_reference<T>::value,
"bad forward of rvalue as lvalue");
return static_cast<T&&>(arg);
}
// 一个普通的模板函数,参数是 T
template <typename T>
void func(T&& param) {
std::cout << "func(T param) called\n";
MyClass obj(forward<T>(param)); // 注意:这里 param 是有名字的变量,一定是左值
}
int main() {
MyClass a;
std::cout << "=== 命名变量(左值) ===\n";
func(a); // 左值 → 拷贝构造
std::cout << "=== 命名变量 + std::move 转为右值 ===\n";
func(std::move(a)); // 传右值,但 param 是左值 → 拷贝构造
return 0;
}
输出如下:
[porco@poyun183010267861 MyTinySTL]$ ./main
=== 命名变量(左值) ===
func(T param) called
left
Copy Constructor
=== 命名变量 + std::move 转为右值 ===
func(T param) called
left
Move Constructor
可以看到输出了两个left,根本就没用到右值的版本。这是因为在func函数里面,param是命名变量,它是左值。那为什么还要定义右值的版本呢?我也还没搞清楚,我本来以为是为了转发函数包,但是实际上转发函数包的时候,也走的是左值的版本。
Demo
#include <iostream>
#include <utility>
#include <type_traits>
class MyClass {
public:
MyClass() {}
// 左值版本
MyClass(const MyClass& a, const MyClass& b) {
std::cout << "Constructor with two LVALUE refs\n";
}
// 右值版本
MyClass(MyClass&& a, MyClass&& b) {
std::cout << "Constructor with two RVALUE refs\n";
}
};
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
std::cout << "left\n";
return static_cast<T&&>(arg);
}
// template <typename T>
// T&& forward(typename std::remove_reference<T>::type&& arg) noexcept {
// std::cout << "right\n";
// static_assert(!std::is_lvalue_reference<T>::value,
// "bad forward of rvalue as lvalue");
// return static_cast<T&&>(arg);
// }
template <typename... Args>
void func(Args&&... args) {
std::cout << "func(Args&&... args) called\n";
MyClass obj(forward<Args>(args)...); // 完美转发两个参数
}
int main() {
MyClass a, b;
std::cout << "=== 两个左值 ===\n";
func(a, b); // 应该走左值版本构造
std::cout << "=== 两个右值 ===\n";
func(MyClass{}, MyClass{}); // 应该走右值版本构造
return 0;
}
输出:
[porco@poyun183010267861 MyTinySTL]$ ./main
=== 两个左值 ===
func(Args&&... args) called
left
left
Constructor with two LVALUE refs
=== 两个右值 ===
func(Args&&... args) called
left
left
Constructor with two RVALUE refs
只有直接在函数体里面用forward才会触发右值的版本,但实际上好像不会这么用forward。
Demo
#include <iostream>
#include <utility>
#include <type_traits>
class MyClass {
public:
MyClass() {}
// 左值版本
MyClass(const MyClass& a, const MyClass& b) {
std::cout << "Constructor with two LVALUE refs\n";
}
// 右值版本
MyClass(MyClass&& a, MyClass&& b) {
std::cout << "Constructor with two RVALUE refs\n";
}
};
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
std::cout << "left\n";
return static_cast<T&&>(arg);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& arg) noexcept {
std::cout << "right\n";
static_assert(!std::is_lvalue_reference<T>::value,
"bad forward of rvalue as lvalue");
return static_cast<T&&>(arg);
}
template <typename... Args>
void func(Args&&... args) {
std::cout << "func(Args&&... args) called\n";
MyClass obj(forward<Args>(args)...); // 完美转发两个参数
}
int main() {
MyClass a, b;
std::cout << "=== 两个左值 ===\n";
func(a, b); // 应该走左值版本构造
std::cout << "=== 两个右值 ===\n";
func(MyClass{}, MyClass{}); // 应该走右值版本构造
std::cout << "=== 直接传右值给 forward(函数外)===\n";
// Here the expression itself is an rvalue; the right overload is selected.
MyClass obj2(forward<MyClass>(MyClass{}), forward<MyClass>(MyClass{}));
// Prints "right" twice and constructs the RVALUE version
return 0;
}
输出如下:
[porco@poyun183010267861 MyTinySTL]$ ./main
=== 两个左值 ===
func(Args&&... args) called
left
left
Constructor with two LVALUE refs
=== 两个右值 ===
func(Args&&... args) called
left
left
Constructor with two RVALUE refs
=== 直接传右值给 forward(函数外)===
right
right
Constructor with two RVALUE refs
C++实现std::move
那C++中move是怎么实现把左值、右值统一转换成右值的呢?move的实现很简单,就是直接用static_cast直接把类型转换成右值的版本。
template <typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
C++实现is_same
#include <iostream>
// 1. 默认情况:两个类型不同
template <typename T, typename U>
struct is_same {
static const bool value = false; // C++11 里用 static const 就行
};
// 2. 偏特化:当两个类型相同时
template <typename T>
struct is_same<T, T> {
static const bool value = true;
};
int main() {
std::cout << std::boolalpha; // 打印 true/false 而不是 1/0
// 验证
std::cout << "is_same<int, int>::value = " << is_same<int, int>::value << '\n'; // true
std::cout << "is_same<int, double>::value = " << is_same<int, double>::value << '\n'; // false
std::cout << "is_same<bool, bool>::value = " << is_same<bool, bool>::value << '\n'; // true
std::cout << "is_same<char, signed char>::value = " << is_same<char, signed char>::value << '\n'; // false
return 0;
}


