这是明确定义的行为吗?
#include <functional>
void foo() {
auto f = new std::function<void()>;
*f = [f]() { delete f; };
(*f)();
f = nullptr;
}
int main() {
foo();
}
使用最新的g ++,如果我在模板中执行此操作,则在valgrind下运行时会导致无效读取,否则它可以正常工作。为什么?这是g ++中的错误吗?
#include <functional>
template<std::size_t>
void foo() {
auto f = new std::function<void()>;
*f = [f]() { delete f; };
(*f)();
f = nullptr;
}
int main() {
foo<0>();
}
答案 0 :(得分:9)
该程序具有明确定义的行为并演示了g ++错误。
运行时唯一可疑的部分是在语句(*f)();
期间。该线的行为可以一块一块地分开。以下标准部分编号来自N3485;如果有些人不符合C ++ 11,那就道歉。
*f
只是指向类类型的原始指针上的内置一元运算符。这里没问题。唯一的另一个评估是函数调用表达式(*f)()
,它调用void std::function<void()>::operator() const
。那个完整表达式就是一个废弃的值。
20.8.11.2.4:
R operator()(ArgTypes... args) const
效果:
INVOKE
(obj, std::forward<ArgTypes>(args)..., R)
其中obj
是*this
的目标对象。
(我已在标准中更换&#34; f
&#34;&#34; obj
&#34;以减少与main
&的混淆#39; s f
。)
此处obj
是lambda对象的副本,ArgTypes
是来自专业化std::function<void()>
的空参数包,R
是void
。< / p>
INVOKE
伪宏在20.8.2中定义。由于obj
的类型不是指向成员的指针,因此 INVOKE
(obj, void)
被定义为obj()
隐式转换为{{1} }}
5.1.2p5:
lambda-expression 的闭包类型有一个公共
void
函数调用操作符...
......具有完整描述的声明。在这种情况下,结果是inline
。它的定义也准确描述了:
5.1.2p7:
lambda-expression 的复合语句会产生函数调用运算符的 function-body ,但是出于目的名称查找,使用
void operator() const
确定this
的类型和值,并将引用非静态类成员的 id-expressions 转换为类成员访问表达式,复合语句在 lambda-expression 的上下文中被考虑。
5.1.2p14:
对于通过复制捕获的每个实体,在闭包类型中声明一个未命名的非静态数据成员。
5.1.2p17:
每个 id-expression 是一个由副本捕获的实体的odr使用,它被转换为对闭包类型的相应未命名数据成员的访问。
因此lambda函数调用运算符必须等效于:
(*this)
(我已经为未命名的lambda类型和未命名的数据成员发明了一些名称。)
该调用运算符的单个语句当然等同于void __lambda_type::operator() const {
delete __unnamed_member_f;
}
所以我们有:
delete (*this).__unnamed_member_f;
取消引用(在prvalue上operator*
)this
表达式
delete
std::function<void()>::~function()
最后,在5.3.5p4中:
delete-expression 中的 cast-expression 应该只评估一次。
(这里是g ++错误的地方,在析构函数调用和释放函数之间的成员子对象上进行第二次值计算。)
此代码在void operator delete(void*)
表达式后不会导致任何其他值计算或副作用。
lambda类型和lambda对象中的实现定义行为有一些限制,但没有一个会影响上面的任何内容:
5.1.2p3:
实现可以定义闭包类型与下面描述的不同,前提是这不会改变程序的可观察行为,只需更改:
封闭类型的大小和/或对齐方式
闭包类型是否可以轻易复制,
闭包类型是标准布局类,还是
闭包类型是否为POD类。
答案 1 :(得分:3)
一般来说,这当然不是明确定义的行为。
在函数对象的执行结束和对operator()
的调用结束之间,成员operator()
正在删除的对象上执行。如果实现通过this
读取或写入(完全允许这样做),那么您将读取或写入已删除的对象。
更具体地说,该对象只是被这个线程删除了,所以在删除和读/写之间任何线程实际上都不可能使用它或者它是未映射的,所以它实际上不太可能导致问题在一个简单的程序中。此外,实现在返回后读取或写入this
的明显原因很少。
但是,Valgrind非常正确,任何此类读取或写入都将非常无效,并且在某些情况下,可能导致随机崩溃或内存损坏。很容易建议,在删除this
和假设读/写之间,此线程被抢占,另一个线程被分配并使用该内存。或者,内存分配器确定它具有足够的此大小的高速缓存内存,并在释放后立即将该段返回给操作系统。这是Heisenbug的一个很好的候选者,因为引起它的条件相对较少,并且只在真正复杂的执行系统中显而易见,而不是琐碎的测试程序。
如果你可以证明在函数对象完成返回后没有读取或写入,你就可以使用它。这基本上意味着保证std::function<Sig>::operator()
的实施。
编辑:
Mats Peterson的回答提出了一个有趣的问题。 GCC似乎已经通过这样的方式实现了lambda:
struct lambda { std::function<void()>* f; };
void lambda_operator(lambda* l) {
l->f->~std::function<void()>();
::operator delete(l->f);
}
正如您所看到的,对operator delete
的调用在刚删除之后从l
加载,这正是我上面描述的情景。我真的不确定C ++ 11的内存模型规则对此有什么说法,我原以为这是非法的,但不一定。它可能无法以任何方式定义。如果它不违法,你肯定是搞砸了。
void lambda_operator(lambda* l) {
auto f = l->f;
f->~std::function<void()>();
::operator delete(f);
}
此处删除l
时无关紧要,因为f
已复制到本地存储中。
在某种程度上,这肯定会回答你的问题 - 在删除后,GCC 绝对从lambda的内存中加载。无论是否合法,我都不确定。您可以通过使用用户定义的函数来解决这个问题。但是,你仍然遇到std :: function实现向this
发送加载或存储的问题。
答案 2 :(得分:2)
问题与lambdas或std :: function无关,而是与delete的语义有关。此代码表现出同样的问题:
class A;
class B {
public:
B(A *a_) : a(a_) {}
void foo();
private:
A *const a;
};
class A {
public:
A() : b(new B(this)) {}
~A() {
delete b;
}
void foo() { b->foo(); }
private:
B *const b;
};
void B::foo() {
delete a;
}
int main() {
A *ap = new A;
ap->foo();
}
问题是删除的语义。是否允许在调用其析构函数后再次从内存加载操作数,以释放其内存?
答案 3 :(得分:2)
请参阅http://cplusplus.github.io/LWG/lwg-active.html#2224。
在析构函数启动后访问库类型是未定义的行为。 Lambda不是库类型,因此它们没有这样的限制。一旦输入了库类型的析构函数,该库类型的不变量就不再成立。该语言不会强制执行此类限制,因为不变量基本上是库概念,而不是语言概念。
答案 4 :(得分:1)
在一般情况下它可能不会崩溃,但是为什么在地球上你会想要首先做这样的事情。
但这是我的分析:
valgrind产生:
==7323== at 0x4008B5: _ZZ3fooILm0EEvvENKUlvE_clEv (in /home/MatsP/src/junk/a.out)
==7323== by 0x400B4A: _ZNSt17_Function_handlerIFvvEZ3fooILm0EEvvEUlvE_E9_M_invokeERKSt9_Any_data (in /home/MatsP/src/junk/a.out)
==7323== by 0x4009DB: std::function<void ()()>::operator()() const (in /home/MatsP/src/junk/a.out)
==7323== by 0x40090A: void foo<0ul>() (in /home/MatsP/src/junk/a.out)
==7323== by 0x4007E8: main (in /home/MatsP/src/junk/a.out)
这指向此处的代码(实际上是原始代码中的lambda函数):
000000000040088a <_ZZ3fooILm0EEvvENKUlvE_clEv>:
40088a: 55 push %rbp
40088b: 48 89 e5 mov %rsp,%rbp
40088e: 48 83 ec 10 sub $0x10,%rsp
400892: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400896: 48 8b 45 f8 mov -0x8(%rbp),%rax
40089a: 48 8b 00 mov (%rax),%rax
40089d: 48 85 c0 test %rax,%rax ;; Null check - don't delete if null.
4008a0: 74 1e je 4008c0 <_ZZ3fooILm0EEvvENKUlvE_clEv+0x36>
4008a2: 48 8b 45 f8 mov -0x8(%rbp),%rax
4008a6: 48 8b 00 mov (%rax),%rax
4008a9: 48 89 c7 mov %rax,%rdi
;; Call function destructor
4008ac: e8 bf ff ff ff callq 400870 <_ZNSt8functionIFvvEED1Ev>
4008b1: 48 8b 45 f8 mov -0x8(%rbp),%rax
4008b5: 48 8b 00 mov (%rax),%rax ;; invalid access
4008b8: 48 89 c7 mov %rax,%rdi
;; Call delete.
4008bb: e8 b0 fd ff ff callq 400670 <_ZdlPv@plt> ;; delete
4008c0: c9 leaveq
4008c1: c3 retq
有趣的是,它的作用和#34;使用clang ++(版本3.5,从git sha1 d73449481daee33615d907608a3a08548ce2ba65构建,从3月31日开始):
0000000000401050 <_ZZ3fooILm0EEvvENKUlvE_clEv>:
401050: 55 push %rbp
401051: 48 89 e5 mov %rsp,%rbp
401054: 48 83 ec 10 sub $0x10,%rsp
401058: 48 89 7d f8 mov %rdi,-0x8(%rbp)
40105c: 48 8b 7d f8 mov -0x8(%rbp),%rdi
401060: 48 8b 3f mov (%rdi),%rdi
401063: 48 81 ff 00 00 00 00 cmp $0x0,%rdi ;; Null check.
40106a: 48 89 7d f0 mov %rdi,-0x10(%rbp)
40106e: 0f 84 12 00 00 00 je 401086 <_ZZ3fooILm0EEvvENKUlvE_clEv+0x36>
401074: 48 8b 7d f0 mov -0x10(%rbp),%rdi
401078: e8 d3 fa ff ff callq 400b50 <_ZNSt8functionIFvvEED2Ev>
;; Function destructor
40107d: 48 8b 7d f0 mov -0x10(%rbp),%rdi
401081: e8 7a f6 ff ff callq 400700 <_ZdlPv@plt> ;; delete.
401086: 48 83 c4 10 add $0x10,%rsp
40108a: 5d pop %rbp
40108b: c3 retq
编辑:它没有任何意义 - 我不明白为什么在gcc的代码中对函数类中的第一个元素进行内存访问而不是在clang&#39中; s - 他们应该做同样的事情......
答案 5 :(得分:0)
分配auto f = new std::function<void()>;
当然没问题。 lambda *f = [f]() { delete f; };
的定义也适用,因为它尚未执行。
现在有趣的是(*f)();
。首先取消引用f
,然后调用operator()
,最后执行delete f
。在类成员函数delete f
中调用function<>::operator()
与调用delete this
相同。 Under certain cirqumstances这是合法的。
所以这取决于operator()
和lamdabs的std::function
实现方式。如果在执行封装的lambda之后保证没有成员函数,成员变量或this指针本身被operator()
使用,那么您的代码将是有效的。
我想说std::function
执行你的lambda之后不需要调用其他成员函数或在operator()
中使用成员变量。因此,您可能会发现您的代码合法的实现,但一般来说,假设这样做可能并不安全。