第一眼看过去感觉就是一个非常泛型的PostEvent函数,把
对象/可调用对象/参数
都模板化了,然后调用的时候再组装起来。可以感觉到用了很多
右值引用
,也能看到用了一些标准库的新模板函数。
这么写的意义是什么?
右值引用与移动语义
1. 为什么要用右值引用以及移动语义
在Cpp里面,一切的最终目的都是为了性能。右值引用解决的是各种情形下
对象的资源所有权转移
的问题。移动语义则是对于
对象构造的性能优化。
右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。
2. 右值
左值(Lvalue):Location-value,表示可寻址。是保存在内存中,
具有确切地址,并能取地址,进行访问,修改等操作的表达式。
右值(Rvalue):Read-value,表示可读不可寻址。是保存在内存中,或者寄存器中,
不知道也无法获得其确切地址,在得到计算表达式结果后就销毁的临时表达式。
int
a = 1; //a是左值,1是右值
const int b = 1; //b是只读的左值,1是右值
a = b + 1; //a是左值,b+2是右值
int x = 0; //x是左值
int* y = &++x; //前置++返回的是左值,可以取地址
++x = 1; //前置++返回的是左值,可以赋值
y = &x++; //后置++返回的是右值,无法取地址和赋值
3. 右值引用
C++11/14开始,使用&&
表示右值引用,&
表示左值引用。
对一个对象使用右值引用就是告诉编译器这个对象是右值,可以被用作转移。这个右值引用也就成了这个对象的别名,意味着对象的生命周期也和这个引用绑定在了一起
,离开作用域后右值对象依然存在。
右值引用只能绑定到临时对象,临时对象大多是字面常量
或者作用域内创建的临时对象
,这些对象都是离开作用域后会被销毁的,也没有所有权归属问题
的对象,这就意味着右值引用可以安全的接管所引用对象的资源。
万能引用const &
依然可以同时引用左值和右值对象。
引用有一些折叠的规则
所有右值引用折叠到右值引用上仍然是一个右值引用。(A&&&& 变成 A&&)
所有的其他引用类型之间的折叠都将变成左值引用。(A&& 变成 A&; A&&& 变成 A&; A&&& 变成 A&)
int& x = ++x; //前置++返回左值,x是左值引用
int&& x = x++; //后置++返回右值, x是右值引用
const int& x = x++; //注意const左值引用也是可以绑定右值的
4. 转移语义
右值引用的最常用的就是实现移动构造函数
与移动赋值运算符重载
,从而实现零成本
构造对象。
标准库函数std::move()
就可以将一个左值强制标记为右值,用作右值引用,本质也是告诉编译器这个对象现在没有所有权
问题了。
class ZeroCost
public:
ZeroCost() = default; // 无参构造函数
~ZeroCost() = default; // 析构函数
ZeroCost(const std::string& InName, TSharedPtr<int> InNum) : Name(std::move(InName)), Num(InNum) {} //带参构造函数
ZeroCost(const ZeroCost& InObject); //拷贝构造函数
ZeroCost(ZeroCost&& InObject) noexcept; //移动构造函数
private:
std::string Name;
std::shared_ptr<int> NumPtr;
ZeroCost::ZeroCost(const ZeroCost& InObject)
this->Name = InObject.Name; //拷贝资源
this->Num = std::shared_ptr<int> TempNumPtr(new int(InObject.Numptr->Get())) //开新地址拷贝资源
ZeroCost::ZeroCost(ZeroCost&& InObject)
Name.empty();
std::swap(Name, InObject.Name); //移动资源,所有权转移
this->NumPtr.reset(InObject.NumPtr); //移动资源, 所有权转移
InObject.NumPtr->reset(); //旧指针可以置空了
int main()
ZeroCheck BaseObj = new ZeroCost();
ZeroCheck CopyObj(BaseObj); //拷贝构造
ZeroCheck AnotherCopyObj = BaseObj; //拷贝构造
ZeroCheck MoveObj = std::move(BaseObj); //移动构造
5.完美转发
当我们将一个右值引用传入函数时,他的实参
有了命名
,所以继续往下传或者调用其他函数时,这个参数变成了一个左值
。那么他永远不会调用接下来函数的右值版本,这可能在一些情况下造成拷贝
。
可以看到GuySomberg在传参的时候使用了一个std::forward()
,这就是C++11提供的完美转发。完美转发实现了参数在传递过程中保持其值属性
的功能,即若是左值,则传递之后仍然是左值,若右值,则传递之后仍然是右值。
完美转发的出现是因为模板参数作为右值引用的时候,编译器会推断
传入的实参属性(引用折叠规则)
来做为实际属性处理。
class ZeroCost
public:
template<typename T>
ZeroCost(T&& InName) : Name{std::forward<T>(InName)} {}
private:
std:string Name;
int main()
const std::string& ObjectName = {"NewObject"};
ZeroCost<std::string> LValueObject(ObjectName);
1. 模板参数传递了一个左值,模板推导T = std::string&
2. T&&&折叠后变为T&,也就是std::string&
3. 构造函数最后形态ZeroCost(std::string& InName) : Name{std::forward<std::string&>(ObjectName)}
4. std::forward<std::string&>(ObjectName)返回的是左值,所以调用的是拷贝构造函数
int main()
ZeroCost<std::string> RValueObject("NewObject");
1. 模板参数传递了一个右值,模板推导T = std::string
2. T&&折叠后变为T&&,也就是std::string&&
3. 构造函数最后形态ZeroCost(std::string&& InName) : Name{std::forward<std::string&&>("NewObject")}
4. std::forward<std::string&&>("NewObject")返回的是右值,所以调用的是移动构造函数
可调用对象
template<typename Fxn, typename ...Ts>
using MemberFunctionReturn = typename std::result_of<Fxn&&(FFMODPlayingEvent&&, Ts&&...)>::type;
GuySombery这段代码非常简练,遇到::
符号的时候要注意用typename表示是一个类型而不是作用域
,要多运用typename和using来优化代码的阅读体验,最重要的是可以大大减少类型更改或者改名的工作量。
这里可以看出Fxn模板参数是需要传入一个Callable Object,对于C++11一定要习惯Callable Object的概念,具体可以参考这篇文章C++中的可调用对象学习,GuySombery这里是把函数作为右值Callable Object来处理
,参考前面说到的完美转发这样声明可以保证传入的Callable Object无论是左值还是右值都可以保证拿到返回值类型。
选择恰当的容器配合标准库中的算法与iterator
,用Callable Object的概念可以实现很多高效且无副作用的函数式编程范式,在一些情况下会非常有用。
用过python的朋友应该很熟悉不定长参数了,c++中以参数包的形式来表示不定长参数,对于模板的不定长参数最常用法就是递归解包了
声明时...类型名
打包,使用时类型名...
解包。需要声明一个递归结束函数。
template<typename T>
void Foo(T arg)
EndOfDoSomthing(arg); //递归解包结束操作
return;
template<typename T, typename ...Ts>
void Foo(T arg, Ts... args) //这里arg就是当前解包出的数据,args是待递归解包的数据
DoSomething(arg); //处理当前解包数据
Foo(args...); // 递归解包
int main()
Foo(1, 1.5, 'a');
auto PlayEventShared = GetPlayingEvent(PlayingEventId);
auto* PlayingEvent = PlayEventShared.Get();
std::unique_ptr<T> Clone(const T& Obj)
return std::unique_ptr<T>(new T(Obj));
再有就是移动语义下,vector插入的时候不再复制操作而是移动的话,就可以在vector里面放unique_ptr了,这就意味着享受了便捷的同时还享受了安全,突然有种在写python的感觉...
template<typename T>
class ManagerMyPtr
public:
void Add2Manager(const T& Obj)
Resources.push_back(Clone(T));
private:
std::vector<unique_ptr<T>> Resources;
自动类型推导
1.为什么需要类型推导
Generic Programming里面涉及到大量的人工很难直接写出的类型
或者伴随未知类型操作的用法
。可是这些类型信息编译器是知道的,只是之前不会暴露给你而已,引用某本书的说法,自动类型推导是将编译器无上的权利赋予了你。
2. auto
auto是运行时的类型推导
,必须时初始化的变量才能推导出来,所以不可用作变量声明。
auto总是推导出值类型!!!
auto&&总是推导出引用类型!!!
auto i = 2; //i为int
auto i = "Hello"; //i为const char*
auto i = m.begin(); //i为iterator类型
auto i = [&](int x){ return x;} //i为Callable Object
float j = 1.f;
auto&& i = j; //i为float引用类型
auto i = std::less<T>(); //i为Callable Object
C++14中auto已经可以推导出表达式返回值类型了!
auto Foo(int x)
return x*x;
3. decltype
decltype是编译时的类型推导
,可以用在变量/类型声明,函数/模板的参数列表等
decltype()获取的是值类型!!!
decltype(())获取的是引用类型!!!
int j = 1;
decltype(j) i = j; //i类型是int
decltype(j)& i = j; //i类型是int&
decltype(*j) i = &j; //i类型是int*
decltype((j)) i = j; //i类型是int&
decltype(std::greater<T>()) MyFuncObj; //声明一个Callable Object
decltype(i)::iterator iter; //推导i的类型再获取其iterator类型
template<typename T>
class Foo {};
Foo<decltype(j)> NewFoo; //相当于Foo<int>()
4. std::result_of<>
template<typename Fxn, typename ...Ts>
using MemberFunctionReturn = typename std::result_of<Fxn&&(FFMODPlayingEvent&&, Ts&&...)>::type;
GuySomberg在这里使用了std::result_of
获取返回值类型,对于可调用对象的推导使用std::result_of在书写上会更优雅一些,本质上std::result_of是可以用decltype实现的。
对于Callable Object
的推导还是推荐像Guy老哥一样使用std::result_of
吧。
GCC4.5中std::result_of的实现
template<typename _Signature>
class result_of;
template<typename _Functor, typename... _ArgTypes>
struct result_of<_Functor(_ArgTypes...)>
typedef decltype( std::declval<_Functor>()(std::declval<_ArgTypes>()...) ) type;