正如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
也是如此。还安全吗
它是否表现出未定义的行为?如果是,标准中具体有哪些内容禁止这样做?
2条答案
按热度按时间h6my8fg21#
tl;dr
要使对象被视为已构造,其初始化必须已完成。
在您的情况下,这意味着构造函数必须已完成执行。
=>
delete this;
直接在一个构造函数中(或者在作为对象构造的一部分调用的函数中)将[几乎]总是未定义的行为。std::stop_callback
,你需要:std::stop_callback
的构造可能直接调用回调,并延迟析构,直到构造完成。std::stop_source::request_stop()
之前注册所有的自销毁std::stop_callback
根本不能保证会请求停止。
如果您的
std::stop_source
上没有发出停止,您的ControlBlock
将简单地泄漏。示例:godbolt
1.法律术语
假设在
doAfterStop(...)
之前调用std::stop_source::request_stop()
,例如:这将基本上调用
Helper::operator()
作为std::stop_callback
的构造函数的一部分,因此这种情况的简化示例可以是:godbolt=>删除作为其构造函数一部分的对象是否法律的?
一个对象的生命周期只有在**初始化完成后才开始:
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)隐式调用析构函数
[...]析构函数也可以通过对 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))的规则,将求值以下之一:
[..]
请注意,这包括了C++ Standard第4-15节中所有类型的未定义行为--唯一没有涵盖的是各种标准库头文件。
不幸的是,这种检查方法并不完全安全,因为它依赖于编译器100%正确地实现C++标准(这很难)。但它仍然是一个很好的检查,可以查看给定的代码是否符合标准。
以下是gcc 12.3、clang 16.0.0和msvc 19.35的结果:
godbolt
请注意,所有3个编译器都拒绝以下代码:
另一方面,如果delete-expression发生在构造完成之后,则所有编译器都接受它:
godbolt
3.潜在修复
解决这个问题的一种方法是检查回调是否在构造函数仍在运行时被调用。
请注意,这种检查必须是线程安全的,因为当我们仍在构造
ControlBlock
的过程中时,回调可能会从另一个线程调用。示例:godbolt
请注意,如果在
std::stop_source
上从未请求停止,此实现仍然会泄漏ControlBlock
。所以你总是需要请求停止,否则
ControlBlock
s可能会泄露。pcww981p2#
通过这个request_stop,我没有看到任何对
std::stop_callback
的析构函数的调用。所以电话是安全的。另外,尝试添加此打印:
在调用
ss.request_stop()
之前,您将获得:之后:
这意味着你有一个标记所有相关的示例。