- 主题:lambd表达式对已析构对象为啥不抛异常
这是你的问题,你一个用例你在栈上创建对象,循环结束后自然会被回收,此时那个指针变成空指针,因此UB。
第二个虽然你使用了共享指针,但你错误传递了共享指针的引用,和上面一样,你还是在引用一个栈空间的变量,其完成后被回收,自然变成空指针。由于此时对象引用计数归0,对象被回收。还是UB。
这个错误非常典型,初学者常犯。其是合法的,lambda没有义务检查空指针。
【 在 Algoquant 的大作中提到: 】
:
: 把对象捕获绑定在lambda表达式里,对象已经析构了,在调用lambda时不抛异常,按UB处理,这是大坑啊。
:
: 我发现对这种情况,只能用 enable_share_from_this 来搞,把对象指针shared_ptr<T>托管到队列或者数组里,异步适时再调用该对象的成员函数。 (我的场景:每个算法对象订阅行情,并在收行情数据后调度器按每个股票快照 调用每个算法对象的onMarketDepth(const MarketDepth& md),这里面就涉及每次调度时,算法对象算法是否还存活)
:
#发自zSMTH@CDU.MP
--
FROM 117.23.183.*
和gc无关,这个就是一个基本错误。在指针指向栈空间变量,但是它最后被回收,下面是一个Python等价示例。
class A:
def __init__(self, i):
print(f"init {type(i)}")
self.i = i
def p(self):
print('>>', self.i)
def __del__(self):
print('del', self)
def main():
a = []
for i in range(3):
ai = A(i)
a.append(lambda : ai.p())
for c in a:
c()
main()
【 在 MyWorkLife 的大作中提到: 】
:
: gc型语言转c++的一般会踩这个坑
:
: 【 在 Algoquant 的大作中提到: 】
: : 标 题: lambd表达式对已析构对象为啥不抛异常
#发自zSMTH@CDU.MP
--
FROM 117.23.183.*
关键不在于传的是什么,在于你的变量是如何申请内存空间的。
基本上出问题的,都是在栈空间初始化这个变量,然后又通过引用捕获,问题是循环完成后栈空间会回收,传递的引用指向的地址上的内存已经被回收了。这时就变成悬空指针了。
这个绝对是标准的错误用法,我个人强烈不建议使用lambda和传统的写法结合,lambda是在函数式编程中兴起的,往上可以追溯到lisp,在函数式编程中,传递的都是值。按照楼上那位的说法,在CPP中就会变为move和rov。
【 在 Algoquant 的大作中提到: 】
:
: 下面的也能跑。结果还是“对”的。
: 闭包里保存引用和指针得万分小心
:
: #include <iostream>
#发自zSMTH@CDU.MP
--
FROM 117.23.183.*
不,实际上在打印时签前面两个已经被析构了。
绑定到第三个是因为循环完成后,这个ai这个变量并没有被删除。或者说,Python并没有在循环中开新的内存空间
你看这个Python示例:
class A:
def __init__(self, i):
print(f"init {type(i)}")
self.i = i
def p(self):
print('>>', self.i)
def __del__(self):
print('del', self)
def main():
a = []
for ai in [A(i) for i in range(3)]:
a.append(lambda : ai.p())
#del ai
del ai
for c in a:
c()
main()
【 在 MyWorkLife 的大作中提到: 】
:
: 你这个例子正好说明问题
: 不像前面例子里面c++ lambda引用一个已经析构的空悬对象
: python对于保持对象生存周期毫无压力
: 只不过三个lambda错误地binding到了最后一个ai对象而已
#发自zSMTH@CDU.MP
--
FROM 117.23.183.*
我相信我对Python的理解是足够的。
事实上,我给出的用例是期望说明在循环中创建的变量是会被回收的,这个用例和CPP lambda传递引用是一致的。
这个代码本身是错误。
【 在 MyWorkLife 的大作中提到: 】
:
: 我觉得你对python闭包捕获对象方式理解不够深
: 建议转python版
:
: 【 在 VincentGe 的大作中提到: 】
#发自zSMTH@CDU.MP
--
FROM 117.23.183.*
我大概理解问题在哪了。
我使希望使用Python代码来说明CPP代码存在的问题的,但是两个在这里有些差异,我混用了一些术语造成误解。
对于Python,在这个示例中,虽然和CPP不同,但在形式上是相同的,在循环中,创建了一个自由变量ai,它被分配在堆上,lambda引用了自由变量ai,但经过三次循环,ai的值被不断改变,最终调用时指向最后一次创建的ai,但如果我们提前del ai或 ai=None,就会导致指向的在堆上的空间被回收,出现错误,你给出的写法,也是Python文档里面的写法,通过复制值的方式,将其传递给lambda,虽然看似相同,但实际上此时ai,被分配在栈上,lambda被调用时,此时ai在新的栈帧中,因此没有问题。
对于CPP,楼主给出了两种错误写法,第一种直接是在栈空间上初始化变量并传递引用给lambda,此时CPP和Python不同,每一次循环时都是新的栈帧,循环结束栈会对变量进行回收,因此那个对象被析构,由于多种原因,三次传递的引用地址指向同一个位置,但在调用时因为对象已经回收,因此实际上他们都是悬空指针。对于第二个用例,虽然使用了共享指针,但却使用引用传递,我猜测这里是因为引用传递没有增加该对象的引用计数,导致其提前被析构,且和上一次同理,都是传递的指针的引用,但是这个指针是在栈空间上,因此都被回收,于是悬空指针,UB。
这两种错误形式非常相似,都是在循环中在lambda中使用变量的引用,因为种种原因,该指针指向的对象被析构,回收且指向的内容发生改变造成的错误,我在这里使用Python用例是期望说明即使使用带有gc语言的开发者,也不会犯这种错误,这种错误真的是低级错误。
更何况,你从直觉上看
for i in ...:
a = A(i)
lambda : a.x()
这种形式难道就不值得怀疑吗?在一个函数内部,两者使用的都是变量的引用,问题是,直观上这个变量的名字在第二次循环上就被改变了。
对于出现这个的原因,我猜测有两点。
一,认为这里lambda获得的对象的引用,但实际上两者对象都是在堆上开辟的(CPP第一个是栈上),除去一个会被回收,剩下的传递都是在循环内变量的引用,而不是对类的引用。无论是,即使是CPP的共享指针,它也需要你捕获这个指针的值。
二,认为这里完全把对象拷贝了,或者说,这里的捕获绑定,把lambda和对象一一绑定,在lambda的栈帧中,那个对象存在,但这样的认知是错误的。
有人提到move和rov ,还有rust的所有权,move等,但是我需要说明的是,在写代码中,如果你不关注这些,那么这些只能期望你的编程规范了,如果你在意它,也不用想rust那样move等等,代码是有明确的含义的,如果出现UB或者非你期望的东西,那么你需要回头看看,绝对是有地方发生 “俺寻思” ,但实际上,代码并非这样。
【 在 z16166 @ [CPlusPlus] 的大作中提到: 】
:
: 这个例子,chatgpt4分析的是延迟绑定问题,不是栈回收问题:
:
: 这段代码的主要问题在于 Python 的闭包延迟绑定。下面是具体的解析:
: 在这段代码中,我们看到在 main() 函数中创建了一个名为 a 的列表,用来存放一组返回 ai.p() 的 lambda 函数。然后在一层 for 循环里,把构建好的 lambda 函数分别放入列表 a 中。然后在下一层 for 循环中,尝试调用这些存储在列表中的函数。
#发自zSMTH@CDU.MP
--
FROM 113.143.107.*
我阅读了那个帖子,非常棒,他讲述的很清晰,感谢你提供的这些。
对于这段Python,我贴出来的原因是它和CPP所犯的错误是相似的,而对于Python的使用者而言,它在Python的文档中的的常见问题中被回答,对于期望深入了解的开发者,可以使用dis包的dis来获取字节码,希望更想深入解释器,那这需要另行讨论。
我期望用此说明楼主所提供的CPP代码中的错误并非由使用带有gc语言所造成的惯性而带来的。
对于CPP这段代码,我希望说明其不抛异常原因并推测其这样做的原因。
【 在 z16166 的大作中提到: 】
:
: 楼主列的C++的例子是非常简单而且典型的,看看汇编代码也能知道
:
: python的代码,不好去看解释器的实现,顶多是加点打印变量地址的代码输出看看,然后就是一些解释器的实现的原则性描述。Ned Batchelder总结了几条,比较清楚,帖子标题是Facts and myths about Python names and values,贴不了URL了
:
#发自zSMTH@CDU.MP
--
FROM 113.143.107.*