【问题标题】:Deleting a std::function object within itself删除自身内部的 std::function 对象
【发布时间】:2018-11-12 22:16:22
【问题描述】:

这是明确定义的行为吗?

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

【问题讨论】:

  • @alphashooter 你能证明吗?规范在哪里说这具有未定义的行为?
  • @SanJacinto 标准不需要说删除 std::function 是已定义的行为。默认情况下(因为删除 std::string 也是如此)。如果这是未定义的行为,IMO 应该需要明确声明在调用绑定函数对象期间删除对象是未定义的。
  • @JohannesSchaub-litb 删除 std::function 不是,但是如何在 std::function 拥有的 lambda 中删除呢?
  • @alphashooter 在这一点上,这种做法的用处对我来说毫无意义。这个问题更多是出于好奇而不是出于实用性。
  • @clin18 好吧,在这种情况下,我可以说以下内容。在您的示例中,在调用期间删除 std::function 不是一个好方法。您可以直接或不直接删除对象的唯一一种情况是,当您确定该对象将永远不会被使用时。没有非静态成员函数调用,没有访问它的字段,什么都没有。如果您确定所有这些条件都成立,那么您可以在任何其他情况下使用它——您不应该这样做。

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


【解决方案1】:

该程序具有明确定义的行为并演示了一个 g++ 错误。

运行时唯一有问题的部分是在语句(*f)(); 期间。那条线的行为可以一块一块地分开。下面的标准部分编号来自 N3485;如果有些与 C++11 不匹配,请致歉。

*f 只是指向类类型的原始指针上的内置一元运算符。这里没问题。唯一的其他评估是函数调用表达式(*f)(),它调用void std::function&lt;void()&gt;::operator() const。那么这个完整的表达式就是一个丢弃的值。

20.8.11.2.4:

R operator()(ArgTypes... args) const

效果:INVOKE(obj, std::forward&lt;ArgTypes&gt;(args)..., R) 其中obj*this 的目标对象。

(我已将标准中的“f”替换为“obj”以减少与mainf 混淆。)

这里obj 是lambda 对象的副本,ArgTypes 是来自特化std::function&lt;void()&gt; 的空参数包,Rvoid

INVOKE 伪宏在 20.8.2 中定义。由于obj 的类型不是指向成员的指针,因此INVOKE(obj, void) 被定义为obj() 隐式转换为void

5.1.2p5:

lambda-expression 的闭包类型有一个公共的 inline 函数调用运算符 ...

... 具有准确描述的声明。在这种情况下,它原来是void operator() const。并且它的定义也有准确的描述:

5.1.2p7:

lambda-expressioncompound-statement 产生函数调用运算符的 function-body,但用于名称查找,确定this 的类型和值,并将引用非静态类成员的id-expressions 转换为使用(*this) 的类成员访问表达式,复合语句lambda-expression 的上下文中考虑。

5.1.2p14:

对于通过副本捕获的每个实体,在闭包类型中声明一个未命名的非静态数据成员。

5.1.2p17:

每个 id-expression 是对通过副本捕获的实体的 odr-use 都转换为对闭包类型的相应未命名数据成员的访问。

所以 lambda 函数调用运算符必须等价于:

void __lambda_type::operator() const {
    delete __unnamed_member_f;
}

(我为未命名的 lambda 类型和未命名的数据成员发明了一些名称。)

那个调用操作符的单条语句当然等价于delete (*this).__unnamed_member_f; 所以我们有:

  • 内置一元 operator* 取消引用(在纯右值 this 上)
  • 成员访问表达式
  • 成员子对象的值计算(也称为左值到右值转换)
  • 标量 delete 表达式
    • 调用std::function&lt;void()&gt;::~function()
    • 调用void operator delete(void*)

最后,在 5.3.5p4 中:

delete-expression 中的cast-expression 应该只计算一次。

(这里是 g++ 错误的地方,在析构函数调用和释放函数之间对成员子对象进行第二次值计算。)

此代码在delete 表达式之后不会导致任何其他值计算或副作用。

在 lambda 类型和 lambda 对象中允许实现定义的行为,但不会影响上述任何内容:

5.1.2p3:

一个实现可以定义不同于下面描述的闭包类型,前提是这不会改变程序的可观察行为,除非通过改变:

  • 闭包类型的大小和/或对齐方式,

  • 闭包类型是否可简单复制,

  • 闭包类型是否为标准布局类,或者

  • 闭包类型是否为 POD 类。

【讨论】:

  • 要回答这个问题,您需要深入研究 g++ 代码。
  • 这并不是真正地调用delete this。它正在删除一个恰好等于this 的指针。这不是const 涵盖的保证。类似地,您可以拥有一个指向对象的指针和指向与 const 相同的对象的指针,然后在普通指针上调用 delete。
  • 这并没有定义在INVOKE表达式返回后std::function::operator()操作符是否可以通过this读写。
  • @DeadMG 我认为标准在这里明确指出INVOKE 表达式必须是operator() 调用的唯一效果。
  • @DeadMG 在 INVOKE 表达式返回后,std::function::operator() 是否可以在列支敦士登启动美国核武库也没有说明。它不需要这样做,因为标准中对行为的描述被认为是完整的。
【解决方案2】:

这肯定不是一般定义明确的行为。

在函数对象的执行结束和对operator()的调用结束之间,成员operator()正在一个已删除的对象上执行。如果实现通过this 读取或写入,这是完全允许的,那么您将读取或写入已删除的对象。

更具体地说,该对象只是被该线程删除,因此任何线程实际上都不太可能在删除和读/写之间使用它,或者它未映射,因此实际上不太可能导致问题在一个简单的程序中。此外,实现在返回后读取或写入this 几乎没有明显理由。

然而,Valgrind 是非常正确的,任何这样的读取或写入都将是非常无效的,并且在某些情况下,可能会导致随机崩溃或内存损坏。很容易提出,在删除this 和假设的读/写之间,该线程被抢占,另一个线程分配并使用了该内存。或者,内存分配器决定它有足够的这种大小的缓存内存,并在释放该段后立即将该段返回给操作系统。这是 Heisenbug 的绝佳候选者,因为导致它的条件相对较少,并且仅在实际复杂的执行系统中而不是琐碎的测试程序中明显。

你可以侥幸成功如果你可以证明在函数对象完成返回后没有读取或写入。这基本上意味着保证std::function&lt;Sig&gt;::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 发出加载或存储的问题。

【讨论】:

  • “如果实现通过它读取或写入,这是完全允许的”。为什么允许这样做?毕竟,如果是这样,那么程序的行为就会变得不确定!
  • @JohannesSchaub-litb 因为是标准库的实现。它是格式良好的,因此它应该具有可预测的行为。如果你使用它,你必须提供它的行为将在你的程序中定义。 使用它。如果在成员函数调用期间允许通过this 读取/写入的对象对您不利,那是您的问题,而不是标准库的问题,如果它格式正确,则与其他任何标准库一样。
  • @Johannes:标准中没有禁止实现在函数对象的operator() 返回后通过this 发出读写操作的措辞。这是实现,所以他们可以实现operator(),但他们喜欢。除非另有明确说明,否则不能假定对已删除对象进行操作是合法的。
  • @JohannesSchaub-litb 我不相信 2224 的决议适用于这种情况。规范性声明“如果标准库类型的对象被访问 [强调我的],并且对象生命周期的开始(3.8 [basic.life])不会在访问之前发生,或者访问不会在对象的生命周期结束之前发生,除非另有说明,否则行为是未定义的。”因为 - 正如 aschepler 的回答中所解释的 - 在operator() 中发生调用后,无法访问std::function。实际访问权限在生命周期内。
  • @Casey 在标准讨论列表中,我和其他人也指出了这一点(这源于对“访问”的草率使用)。这仍然是一个未解决的问题报告,因此那里提出的解决方案不一定能以高概率反映委员会的意见(就像放入规范中的问题解决方案一样)。与规范中 的注释不同,它确实 很有可能表达了委员会的意图,而不是规范地。在我们的例子中,规范中的注释使这个问题中的代码未定义。
【解决方案3】:

问题与 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();
}

问题在于删除的语义。是否允许在调用其析构函数后再次从内存中加载操作数,以释放其内存?

【讨论】:

  • 很好的发现。在这种情况下,这一切都归结为 5.3.5p4 “delete-expression 中的 cast-expression 应只计算一次。”我将“评估”阅读为包括所有类型的操作 3.8 禁止在指向/glvalue 的指针上引用已破坏的对象。
  • @aschepler:我认为您的阅读是正确的。这实际上是 clin18 为我教授的课程提交的一些代码。我们试图追查是他的代码不合规还是 g++ 不合规。 (我们知道 clang++ 很好用。)
【解决方案4】:

http://cplusplus.github.io/LWG/lwg-active.html#2224

在析构函数启动后访问库类型是未定义的行为。 Lambda 不是库类型,因此它们没有这样的限制。一旦输入了库类型的析构函数,该库类型的不变量就不再成立。该语言没有强制执行这样的限制,因为不变量基本上是一个库概念,而不是一个语言概念。

【讨论】:

    【解决方案5】:

    在一般情况下它可能不会崩溃,但你到底为什么要首先做这样的事情。

    但这是我的分析:

    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   
    

    有趣的是,它使用 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 的代码中对函数类中的第一个元素有内存访问——它们应该做同样的事情。 ..

    【讨论】:

    • 有趣——编译器加载了f 两次。我不确定这在 C++11 内存模型下是否合法。
    • 查看(相当混乱 - 我确信有充分理由)功能性头文件,似乎涉及一些 _M_access (但也许只有当函数具有实际参数时?)。
    【解决方案6】:

    分配auto f = new std::function&lt;void()&gt;;当然没问题。 lambda *f = [f]() { delete f; }; 的定义同样有效,因为它尚未执行。

    现在有趣的是(*f)();。首先它取消引用f,然后调用operator(),最后执行delete f。在类成员函数function&lt;&gt;::operator() 中调用delete f 与调用delete this 相同。 Under certain cirqumstances 这是合法的。

    所以这取决于operator() for std::function 和 lamdabs 是如何实现的。如果保证在执行封装的 lambda 后没有任何成员函数、成员变量或 this 指针本身被 operator() 使用甚至触及,则您的代码将是有效的。

    我想说std::function 在执行您的 lambda 后无需调用其他成员函数或使用 operator() 中的成员变量。所以你可能会找到你的代码是合法的实现,但一般来说,假设这样可能是不安全的。

    【讨论】:

      猜你喜欢
      • 2014-08-27
      • 1970-01-01
      • 2016-03-27
      • 1970-01-01
      • 1970-01-01
      • 2012-08-21
      • 2017-11-20
      • 1970-01-01
      相关资源
      最近更新 更多