最近看了《Effective Morden C++》,把C++11移动语义及相关内容简单梳理了一下,并用代码进行了测试。
欢迎感兴趣的同行一起交流。
#include <vector>
#include <testngpp/testngpp.hpp>
using namespace std;
namespace {
// 传入左值时,会走这个重载函数
template<typename T>
const char* get_type(T &) {
return "lvalue";
}
// 传入右值时,会走这个重载函数
template<typename T>
const char* get_type(T &&) {
return "rvalue";
}
template<typename T>
const char* UniversalRefFuncParam(T&& param) {
return get_type(param); // 参数永远是左值
}
template<typename T>
const char* UniversalRefFuncForward(T&& param) {
// 传给UniversalRefFuncForward的参数的左右值类型,会forward给get_type
// 原理是:根据推导出的T的类型
// 如果 实参 是 左值, 则传给通用引用时,推导出的 T 为左值引用
// 如果 实参 是 右值, 则传给通用引用时,推导出的 T 为右值引用
// 然后根据 引用折叠 规则,只有两个引用都是右值引用,结果才是右值引用;否则是左值引用
// 就得到 forward 转换后的类型, 左值引用,或者右值引用, 保留了传入的实参的左右值类型
return get_type(std::forward<T>(param));
}
template<typename T>
const char* UniversalRefFuncMove(T&& param) {
return get_type(std::move(param)); // 总是把param转换为右值,再调用get_type
}
}
FIXTURE(CppMoveIntro, C++11引入移动语义的背景)
{
TEST(使用临时对象构造另外一个对象,直接移动,从而提高效率)
{
INFO("引入右值 - 临时对象,字面量,表达式计算结果等 : 它们不会多次复用,可以移动");
INFO("引入左值 - 能够获取地址的对象,或者命名的对象 : 它们可能多次复用,所以不能随意移动");
INFO("引入移动构造, 移动赋值 - 即参数为右值类型 如 int&&");
INFO("引入 std::move 类型转换 - 即把参数转换为右值类型");
INFO("引入 std::forward 类型转换 - 即把传给通用引用的参数的左右值类型,继续传递给调用的函数");
INFO("基本概念1: 左值引用 形式 - int&, T&");
INFO("基本概念2: 右值引用 形式 - int&&");
INFO("基本概念3: 通用引用 形式 - T&& 且 T 需要进行类型推导");
INFO("使用规则1: 通用引用参数再往下传时用forward转换");
INFO("使用规则2: 右值引用参数再往下传时用move转换");
INFO("使用规则3: 明确不再使用的对象,传递给别的函数时,才使用forward/move转换。避免移动后它的值改变了,后面又用到它。");
INFO("参考《Effective Morden C++》 - 第五章 右值引用,Move语义和完美转发");
}
};
FIXTURE(LValue, 典型的左值)
{
TEST(变量是左值)
{
int var = 10;
ASSERT_STREQ("lvalue", get_type(var));
}
TEST(字符串常量是左值 - 因为它是存储在char数组中,可以取地址)
{
ASSERT_STREQ("lvalue", get_type("This is test string"));
}
TEST(++a是左值 - a先自加,再返回a)
{
int a = 10;
ASSERT_STREQ("lvalue", get_type(++a));
}
TEST(a++是右值 - 先返回a的值[实际是个临时变量],然后a自加)
{
int a = 10;
ASSERT_STREQ("rvalue", get_type(a++));
}
int int_member = 10;
int& get_ref() {
return int_member;
}
TEST(返回引用的函数,它返回的是左值)
{
ASSERT_STREQ("lvalue", get_type(get_ref()));
}
};
FIXTURE(RValue, 典型的右值)
{
TEST(字面量是右值)
{
ASSERT_STREQ("rvalue", get_type(10));
}
TEST(左值强制类型转换为一个非引用的值 是 右值)
{
int var = 10;
ASSERT_STREQ("rvalue", get_type(static_cast<double>(var)));
}
TEST(使用统一初始化构造的对象[非变量或成员定义] 是 右值)
{
ASSERT_STREQ("rvalue", get_type(double{}));
ASSERT_STREQ("rvalue", get_type(std::vector<int>{}));
}
TEST(表达式运算结果是右值)
{
ASSERT_STREQ("rvalue", get_type(1+2));
int a = 10, b = 20;
ASSERT_STREQ("rvalue", get_type(a+b));
}
int get_val() {
return 10;
}
TEST(返回值的函数,它返回的是右值)
{
ASSERT_STREQ("rvalue", get_type(get_val()));
}
};
FIXTURE(FuncParamAlwaysLValue, 函数的参数都是左值)
{
TEST(函数的参数都是左值,它们体现为形参变量,能取地址,当然是左值 - 跟传入的参数类型无关)
{
ASSERT_STREQ("lvalue", UniversalRefFuncParam(1)); // 传入的参数1,字面量,是右值
int var = 10;
ASSERT_STREQ("lvalue", UniversalRefFuncParam(var)); // 传入的参数var,是左值
}
};
FIXTURE(ForwardRealParamLRValueAttribute, forward传递实参左右值类型)
{
TEST(forward可以把调用UniversalRefFuncForward时传入的实参的左右值类型继续传递给调用的get_type)
{
ASSERT_STREQ("rvalue", UniversalRefFuncForward(1)); // 传入的参数1,字面量,是右值
int var = 10;
ASSERT_STREQ("lvalue", UniversalRefFuncForward(var)); // 传入的参数var,是左值
}
};
FIXTURE(MoveAlwaysCastParamToRValue, move总是把param转换为右值)
{
TEST(move实际是把对象转换为右值 - param本来是左值,调用move后转换为了右值 - 与外部传入的参数类型无关)
{
ASSERT_STREQ("rvalue", UniversalRefFuncMove(1)); // 传入的参数1,字面量,是右值
int var = 10;
ASSERT_STREQ("rvalue", UniversalRefFuncMove(var)); // 传入的参数var,是左值
}
};
这个代码可以在 testngpp 或者 testngpp2 下编译运行。MSVC编译运行显示如下内容:
参考:
https://github.com/sinojelly/mockcpp
https://github.com/sinojelly/testngpp2