内容简介:内容概要Lambda可以让代码简化很多,可维护性也能提高很多,但是它也有一些小细节,不小心的话,可能程序Crash了还不知道是哪里的问题。【欢迎转载,请注明作者:房燕良,原文出处:游戏程序员的自我修养】
内容概要
Lambda可以让代码简化很多,可维护性也能提高很多,但是它也有一些小细节,不小心的话,可能程序Crash了还不知道是哪里的问题。
【欢迎转载,请注明作者:房燕良,原文出处:游戏 程序员 的自我修养】
C++ Lambda 基础知识
Lambda ,就是希腊字母“ λ ”,据说是代表着“λ演算(lambda calculus)”。C++11开始支持Lambda,可以说它只是一个便利机制。Lambda能做的事情,本质上都可以手写代码完成,但是它确实太方便了!怎么说呢,还好以前没有认真学std::bind各种绕法,现在用lambda方便多了。
我们可以通过简单的例子初步认识一下:
int var1 = 100; std::string var2 = "hello"; auto myLambda = [var1, &var2](int param) -> std::string { var2.append(std::to_string(var1)); var2.append(std::to_string(param)); return var2; }; std::cout << "fistLambda typeid = " << typeid(myLambda).name() << std::endl;
上面代码中由 []
开头的那一串就是lambda了。在大多数情况下我们就使用“lambda”这个名词就够了,但其实仔细想想,其中代码涉及到三个概念:
- lambda表达式(lambda expression)
- 闭包(closure)
- 闭包类(closure class)
例如,在上面这段代码中:
- 定义了一个变量:myLambda,它就是“闭包”
- myLambda 的类型是一个编译器生成的匿名的类,也就是“闭包类”;
-
这个闭包类是有等号右边的”lambda表达式”生成的,这个lambda表达式:
- 按值捕获了var1;按引用捕获了var2;
- 并且接受一个int型参数;
- 返回一个std::string对象
我们可以尝试把编译器自动生成的”闭包类”写出来,把“闭包”对象的构造也写出来,就应该能说明问题了。下面这段代码大体上和上面的代码等效:
int var1 = 100; std::string var2 = "hello"; class MyClosureClass { int var1; std::string& var2; public: MyClosureClass(int inVar1, std::string& inVar2) : var1(inVar1), var2(inVar2) {} // not default constructible MyClosureClass() = delete; MyClosureClass(const MyClosureClass&) = default; MyClosureClass(MyClosureClass&&) = default; ~MyClosureClass() = default; // not copy assignable MyClosureClass& operator=(const MyClosureClass&) = delete; // function-call operator std::string operator()(int param) { var2.append(std::to_string(var1)); var2.append(std::to_string(param)); return var2; } }; auto myLambda = MyClosureClass(var1, var2); std::cout << "myLambda: " << myLambda(2233) << std::endl;
class MyClosureClass 还可能包含一个自定义的类型转换操作符,用来把闭包对象转换成函数指针。
捕获列表“有坑”
lambda表达式的常用语法格式如下:
[ captures ] ( params ) -> return_type { body }
为了理解方便,只列出了常用元素,不全面。
其中比较值得一说的就是 [captures]
:捕获列表了!
[captures]
支持多种写法,首先就是个人不推荐使用的两种默认捕获模式(default capture modes):
[=] [&]
从性能、代码可维护性等方面都不建议使用这两种方式。比较常用的写法就是明确列出需要捕获的变量,例如: [var1, &var2]
, 其中 var1
使用了“按值捕获”模式, var2
前面加了一个 &
代表着它使用“按引用捕获”的模式。下面就分别讨论一下“按值捕获”和“按引用捕获”有什么坑。
按值捕获 & 捕获时机
按值捕获就是在创建闭包的时候,将当前作用域内的变量赋值到闭包类的成员变量中,这个比较好理解,但是也有一个小小的坑。请看下面代码:
FString LocalStr = TEXT("First string"); auto TestLambda = [LocalStr]() { UE_LOG(LogTemp, Error, TEXT("String = %s ."), *LocalStr); }; LocalStr = TEXT("Second string"); TestLambda();
当调用 TestLambda()
的时候,也许会觉得意外,输出的还是:String = First string。这就是要注意的地方,当闭包生成的那一刻,被捕获的变量已经按值赋值的方式进行了捕获,后面那个 LocalStr
对象再怎么变化,已经和闭包对象里面的值没有关系了。
如果按引用捕获,则可以跟踪 LocalStr
的更新了,但是按引用捕获的坑更深。
按引用捕获 & 悬空引用
如果是在C#中使用 lambda 就简单很多了,它有自动垃圾回收、class对象全部是引用类型这些特性,而对于C++来说,对象的生命周期、内存管理这根弦始终要绷紧。在C++编程中, 程序员有责任保证Lambda调用的时候,保证被捕获的变量仍然有效 ~!是的,责任在你,而不在编译器。如果不能很好理解这点,就会遇到悬空引用的问题!
悬空引用( dangling references )就是说我们创建了一个对象的引用类型的变量,但是被引用的对象被析构了、无效了。一般情况下,引用类型的变量必须在初始化的时候赋值,很少遇到这种情况,但是如果lambda被延迟调用,在调用时,已经脱离了当前的作用域,那么按引用捕获的对象就是悬空引用。
我们先来看一段代码:
FString LocalStr = TEXT("Local string"); auto TestLambda = [&LocalStr]() { UE_LOG(LogTemp, Error, TEXT("String = %s ."), *LocalStr); LocalStr = TEXT("Lambda string"); }; // 在这里直接调用是没问题的 TestLambda(); // 在Timer中调用,妥妥的Crash! FTimerDelegate Delegate; Delegate.BindLambda(TestLambda); FTimerHandle TestTimer; GetWorldTimerManager().SetTimer(TestTimer, Delegate, 1.0f, true);
上面这段的代码,在定义lambda之后立即调用则可以运行,同样一个labmda放入timer则会crash!这是为什么呢?
前面基本概念那一部分讲到了 TestLambda
是一个闭包对象,它的类型是编译器生成的一个匿名的class。对于这个例子,我尝试把这个闭包类的核心部分写出来:
class MyLambdaClass { FString& LocalStr; public: MyLambdaClass(FString& InLocalStr) :LocalStr(InLocalStr) {} void operator()() const { UE_LOG(LogTemp, Error, TEXT("String = %s ."), *LocalStr); LocalStr = TEXT("Lambda string"); } };
看到上面这个class,应该就很清晰了:
-
TestLambda()
直接调用那一句,FString LocalStr
这个对象还在作用域内,所以可以执行; -
而在Timer执行的时候,
LocalStr
这个对象已经出了作用域,被析构了,这个时候Lambda中捕获的那个引用就变成了 悬空引用 啦,所以会导致Crash!
总之,使用各种 Delegate 的 “BindLambda” 的时候,要格外小心悬空引用的风险。
捕获UObject指针
虚幻的UObject具备自动垃圾回收机制,但这个机制是基于对象之间的引用关系的,也就是说一个 UObject 指针被捕获之后,还是可能被垃圾回收的。所以,对于延迟调用的lambda是不建议捕获UObject的;如果实在需要的话建议使用 FWeakObjectPtr ,例如这样:
AActor* TargetActor = FindMyTargetActor(); auto ObjectLambda = [ActorPtr = TWeakObjectPtr<AActor>(TargetActor)](const FVector& Offset) { if (ActorPtr.IsValid()) { AActor* TargetActor = ActorPtr.Get(); TargetActor->AddActorWorldOffset(Offset); } };
通过 FWeakObjectPtr 引用 UObject 指针不会影响对象的生命周期,在 FWeakObjectPtr::IsValid()
方法中默认会判断当前对象是不是 “Pending Kill” 状态。
如果希望持有某个UObject的强引用,保证它不被垃圾回收,那么建议不要用lambda,建议使用其他写法:
- 使用 Delegate 的 BindUObject 或者 BindUFunction 来处理;
-
如果是很复杂的代码,也可以用
UObject
或者FGCObject
的派生类来处理。
C++14的初始化捕获(init capture)
在上面UObject指针的例子中,捕获列表是这样写的: ActorPtr = TWeakObjectPtr<AActor>(TargetActor)
,这种写法就是C++14引入的新特性“初始化捕获”,也被称为广义捕获(generalized capture)。这个的语法是这样的:
- 等号左边的变量是声明在“闭包类” 里面的,它的类型由编译器自动推导;
- 等号右边的表达式,其作用域就是当前定义lambda的作用域,可以引用局部变量或者实参。
这个语法更有用的地方是:它可以把使用“移动语义”把局部变量移动到闭包中,类似这样:
FString SomeBigString; // ... auto MyLambda = [MyStr = MoveTemp(SomeBigString)] { //.... };
延伸阅读
- Unreal Engine Coding Standard
- Lambda expressions (since C++11) , cppreference.com
Written on March 1st , 2020 by 房燕良
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 同态加密的现实与虚幻
- Unreal Engine(虚幻引擎)4.25 发布
- Unreal Engine 4.23 发布,虚幻引擎
- 虚幻引擎 (Unreal Engine) 5 发布抢先体验版
- 虚幻4与现代C++:基于任务的并行编程与TaskGraph
- 虚幻4与现代C++:基于任务的并行编程与TaskGraph
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Flexible Rails
Peter Armstrong / Manning Publications / 2008-01-23 / USD 44.99
Rails is a fantastic tool for web application development, but its Ajax-driven interfaces stop short of the richness you gain with a tool like Adobe Flex. Simply put, Flex is the most productive way t......一起来看看 《Flexible Rails》 这本书的介绍吧!