c++ 自销毁std::stop_callback对象可以在构造过程中销毁自己吗?

093gszye  于 2023-05-20  发布在  其他
关注(0)|答案(2)|浏览(98)

正如cppreference.com(link)上所说,std::stop_callback对象可以在其回调函数中销毁自己。下面的代码应该没问题。

template <typename F>
void doAfterStop(std::stop_token && st, F f)
{
    struct ControlBlock
    {
        F f;
        struct Helper
        {
            ControlBlock * cb;
            void operator()()
            {
                cb->f();
                delete cb;
            }
        };
        std::stop_callback<Helper> sc;
        ControlBlock(std::stop_token && st, F f)
            : f{ f }
            , sc{ std::move(st), Helper{this} }
        {}
    };
    new ControlBlock{ std::move(st), f };
}

int main()
{
    std::stop_source ss;
    //ss.request_stop();//?
    doAfterStop(ss.get_token(), []{ std::puts("Byebye, world!"); });
    ss.request_stop();
    return 0;
}

但是如果我把ss.request_stop();行放在doAfterStop(...);行之前呢?
std::stop_callback的析构函数将在它自己的构造函数中被调用。ControlBlock也是如此。还安全吗
它是否表现出未定义的行为?如果是,标准中具体有哪些内容禁止这样做?

h6my8fg2

h6my8fg21#

tl;dr

  • delete-expression的操作数必须是一个构造对象。

要使对象被视为已构造,其初始化必须已完成。
在您的情况下,这意味着构造函数必须已完成执行。
=> delete this;直接在一个构造函数中(或者在作为对象构造的一部分调用的函数中)将[几乎]总是未定义的行为。

  • 如果你想要一个自销毁的std::stop_callback,你需要:
  • (a)处理边缘情况,即std::stop_callback的构造可能直接调用回调,并延迟析构,直到构造完成。
  • (B)确保在调用std::stop_source::request_stop()之前注册所有的自销毁std::stop_callback
  • 请注意,您仍然有一个未处理的edge-case:

根本不能保证会请求停止。
如果您的std::stop_source上没有发出停止,您的ControlBlock将简单地泄漏。
示例:godbolt

1.法律术语

假设在doAfterStop(...)之前调用std::stop_source::request_stop(),例如:

int main()
{
    std::stop_source ss;
    ss.request_stop();
    doAfterStop(ss.get_token(), []{ std::puts("Byebye, world!"); });
    return 0;
}

这将基本上调用Helper::operator()作为std::stop_callback的构造函数的一部分,因此这种情况的简化示例可以是:godbolt

struct Bar {
    Bar() { delete this; }
    ~Bar() { }
};

int main() {
    new Bar();
}

=>删除作为其构造函数一部分的对象是否法律的?
一个对象的生命周期只有在**初始化完成后才开始:
6.7.3使用寿命[basic.life](强调我的)
(1)[...] T类型的对象的生存期开始于:
(1.1)- 获得具有用于类型T的适当对准和大小的存储,以及
(1.2)-初始化(如果有)完成(包括空初始化)(dcl.init(https://timsong-cpp.github.io/cppwp/n4868/dcl.init))
对于ControlBlock类,执行的初始化将是 direct-initialization,这涉及到调用构造函数:
9.4.1概述[dcl.init.general](着重号为我的)
(16.6.2)[...]如果初始化是直接初始化,[...],则考虑构造函数。枚举适用的构造函数,并通过重载解析选择最佳构造函数。
然后:
(16.6.2.1)重载解析成功,调用所选构造函数初始化对象,参数为初始化表达式或表达式列表。
所以当你的构造函数正在运行时,对象还没有在它的生存期内,因为初始化还没有完成。
因此,尝试在构造函数中以this作为操作数执行delete-expression将违反 6.7.3 Lifetime(6)
6.7.3使用寿命[basic.life](强调我的)
(6)在一个对象的生存期开始之前,但在该对象将占用的存储空间被分配[...]之后,任何表示该对象将位于或曾经位于的存储位置的地址的指针都可以被使用,但只能以有限的方式使用。[...]

程序有未定义行为,如果:

(6.1)对象将是或曾经是具有非平凡析构函数的类类型,并且指针用作 delete-expression的操作数
注1:std::stop_callback有一个用户定义的析构函数,所以它绝对不是普通的析构函数。您的ControlBlock有一个std::stop_callback类型的数据成员,这导致它的析构函数也是非平凡的。
注2:对于类对象,6.7.3 Lifetime(6)有一些例外,但它们都不允许delete-expressions:11.9.5 Construction and destruction
所以你的例子会导致未定义的行为,因为它试图删除一个对象[使用一个非平凡的析构函数],而这个对象还没有在它的生存期内。
或者,我们也可以定义隐式析构函数调用来得出相同的结论:
(Note导致隐式析构函数调用的所有方式(包括delete-expressions)都需要一个构造对象**)
11.4.7析构函数[class.dtor](强调我的)
(15)隐式调用析构函数

  • (15.1)对于在程序终止时具有静态存储持续时间的构造对象
  • (15.2)对于构造对象,线程退出时有线程存储期
  • (15.3)对于构造的对象,当创建对象所在的块退出时,自动保存时间,
  • (15.4)对于构造的临时对象,当其生命周期结束时

[...]析构函数也可以通过对 new-expression [...]分配的构造对象使用 delete-expression 来隐式调用

  • 构造对象 * 的定义如下:

9.4.1概述[dcl.init.general](着重号为我的)
(20)* * 初始化完成的对象被视为构造**。[...]
因此,假设对构造函数的调用是类初始化的一部分,并且由于在构造函数中执行了delete表达式,操作数还不能是构造对象。(它只有在初始化完成后才成为构造对象)
=>所以你的例子会导致UB,因为它违反了隐式析构函数调用的前提条件,它调用了一个 * 构造的对象 *。

2.编译器检查

C++常量表达式的一个好处是,我们可以(ab-)使用它们来测试未定义的行为。
大多数未定义的行为在常量表达式中是病态的,所以编译器必须产生编译错误,如果他们在需要常量表达式的上下文中遇到这种未定义的行为。
7.7常量表达式[expr.const](强调我的)
(5)一个表达式 E 是一个 * 核心常量表达式 *,除非 E 的求值,遵循抽象机(intro.execution(https://timsong-cpp.github.io/cppwp/n4868/intro.execution))的规则,将求值以下之一:
[..]

  • (5.7)具有未定义行为的操作,如intro(https://timsong-cpp.github.io/cppwp/n4868/intro)到cpp(https://timsong-cpp.github.io/cppwp/n4868/cpp)中所规定的;

请注意,这包括了C++ Standard第4-15节中所有类型的未定义行为--唯一没有涵盖的是各种标准库头文件。
不幸的是,这种检查方法并不完全安全,因为它依赖于编译器100%正确地实现C++标准(这很难)。但它仍然是一个很好的检查,可以查看给定的代码是否符合标准。
以下是gcc 12.3、clang 16.0.0和msvc 19.35的结果:
godbolt

struct Bar {
    constexpr Bar() { delete this; }
    constexpr ~Bar() { }
};

consteval void foo() {
    new Bar();
}

int main() {
    foo();
}

请注意,所有3个编译器都拒绝以下代码:

  • gcc:
error: deallocation of storage that was not previously allocated
  • clang:
note: destruction of object that is already being destroyed
  • msvc:
note: failure was caused by deallocation of pointer not allocated on the heap

另一方面,如果delete-expression发生在构造完成之后,则所有编译器都接受它:
godbolt

struct Bar {
    constexpr Bar() { }
    constexpr ~Bar() { }

    constexpr void yeet() {
        delete this; 
    }
};

consteval void foo() {
    (new Bar())->yeet();
}

int main() {
    foo();
}

3.潜在修复

解决这个问题的一种方法是检查回调是否在构造函数仍在运行时被调用。
请注意,这种检查必须是线程安全的,因为当我们仍在构造ControlBlock的过程中时,回调可能会从另一个线程调用。
示例:godbolt

#include <atomic>
#include <stop_token>

template <class F>
void doAfterStop(std::stop_token const& st, F f) {
    struct ControlBlock {
        enum State {
            // constructor is running
            Constructing = 0,
            // callback has been called while constructor is running
            SyncDestruct = 1,

            // constructor has finished
            Constructed = 2,
            // callback has been called after constructor finished
            DirectDestruct = 3
        };

        struct Helper {
            ControlBlock* cb;

            void operator()() const {
                cb->f();

                int state = cb->state.fetch_add(1, std::memory_order_acq_rel) + 1;
                if(state == State::DirectDestruct) {
                    delete cb;
                }
            }
        };

        // order is important!
        // state and f must be constructed before sc!
        std::atomic<int> state;
        F f;
        std::stop_callback<Helper> sc;
        

        ControlBlock(std::stop_token const& st, F&& f)
            : state{ State::Constructing },
              f{ std::move(f) },
              sc{ st, Helper{this} }
        { }

        void checkDestructImmediately() {
            int expected = State::Constructing;
            bool res = state.compare_exchange_strong(expected, State::Constructed, std::memory_order_acq_rel);
            if(!res) {
                delete this;
            }
        }
    };

    ControlBlock* cb = new ControlBlock{ st, std::move(f) };
    cb->checkDestructImmediately();
}

请注意,如果在std::stop_source上从未请求停止,此实现仍然会泄漏ControlBlock
所以你总是需要请求停止,否则ControlBlock s可能会泄露。

pcww981p

pcww981p2#

通过这个request_stop,我没有看到任何对std::stop_callback的析构函数的调用。所以电话是安全的。
另外,尝试添加此打印:

auto print = [](const std::stop_source &source) {
    std::printf("stop_source stop_possible = %s, stop_requested = %s\n",
                source.stop_possible() ? "true" : "false",
                source.stop_requested() ? "true" : "false");
};

在调用ss.request_stop()之前,您将获得:

stop_source stop_possible = true, stop_requested = false

之后:

stop_source stop_possible = true, stop_requested = true

这意味着你有一个标记所有相关的示例。

相关问题