删除自身内的std :: function对象

时间:2014-04-10 20:29:04

标签: c++ c++11 lambda std-function

这是明确定义的行为吗?

#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>();
}

6 个答案:

答案 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()>的空参数包,Rvoid。< / 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的内存模型规则对此有什么说法,我原以为这是非法的,但不一定。它可能无法以任何方式定义。如果它不违法,你肯定是搞砸了。

但是,Clang似乎生成了这个运算符:

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()中使用成员变量。因此,您可能会发现您的代码合法的实现,但一般来说,假设这样做可能并不安全。