class 构造函数可以永远阻塞吗?
Is it OK for a class constructor to block forever?
假设我有一个在无限循环中提供某种功能的对象。
将无限循环放在构造函数中是否可以接受?
示例:
class Server {
public:
Server() {
for(;;) {
//...
}
}
};
或者如果构造函数从未完成,C++ 是否存在固有的初始化问题?
(想法是,对于 运行 一个服务器,你只需说 Server server;
,可能在某处的线程中...)
这是允许的。但与任何其他无限循环一样,它 must have observable side effects, otherwise you get undefined behavior.
调用网络函数算作“可观察到的副作用”,所以你是安全的。此规则仅禁止那些实际上什么都不做或只是在不与外界交互的情况下随机播放数据的循环。
按照标准并没有错,只是设计不好。
构造函数通常不会阻塞。它们的目的是获取原始内存块,并将其转换为有效的 C++ 对象。析构函数恰恰相反:它们获取有效的 C++ 对象并将它们转回原始内存块。
如果你的构造函数永远阻塞(强调永远),它所做的事情不同于仅仅将一块内存变成一个对象。
可以短时间阻塞(互斥锁就是一个很好的例子),如果这有助于对象的构造。
在您的情况下,看起来您的构造函数正在接受和服务客户。这不是把内存变成对象。
我建议您将构造函数拆分为一个构建服务器对象的“真实”构造函数和另一个为客户端提供服务的 start
方法(通过启动事件循环)。
ps:在某些情况下,您 必须 与构造函数分开执行对象的 functionality/logic,例如,如果您的 class 继承自 std::enable_shared_from_this
.
它是合法的,但最好避免它。
主要问题是您应该避免让用户感到意外。拥有一个从不 returns 的构造函数是不寻常的,因为它不合逻辑。你为什么要构建你永远无法使用的东西?因此,虽然该模式可能有效,但它不太可能是预期的行为。
第二个问题是它限制了您的服务器 class 的使用方式。 C++ 的构造和破坏过程是该语言的基础,因此劫持它们可能很棘手。例如,一个人可能希望 Server
是 class 的成员,但现在总体 class' 构造函数将阻塞......即使这不直观。这也使得将这些对象放入容器变得非常困难,因为这可能涉及分配许多对象。
我能想到的最接近你所做的是std::thread
。 Thread 不会永远阻塞,但它确实有一个构造函数可以完成大量工作。但是如果你看看 std::thread
,你就会意识到,当谈到多线程时,惊讶是常态,所以人们在这样的选择上的麻烦就更少了。 (我个人并不清楚在构造时启动线程的原因,但是多线程中有很多极端情况,如果它解决了其中一些我不会感到惊讶)
用户可能希望在主线程中设置您的 Server
对象。然后在工作线程中调用 server.endless_loop()
函数。
在实际服务器中,获取端口的过程需要升级权限,然后可以删除。或者您有一个需要加载设置的对象。在其他地方发生长期循环之前,这些任务可能发生在主线程中。
就个人而言,我更希望您的对象具有快速且非阻塞的“轮询”功能。然后你可以有一个循环函数,在无限循环中调用 poll 和 sleep 。您甚至可能有一个原子变量,您可以将其设置为从不同的线程退出循环。另一个功能是在服务器对象中启动一个内部线程。
正如其他人所指出的,就 C++ 语义而言,这并没有什么“错误”,但它的设计很糟糕。构造函数的目的是构造一个对象,因此如果该任务永远不会完成,那么用户将感到惊讶。
其他人提出了关于将构造和 运行 步骤拆分为构造函数和方法的建议,如果除了 运行 它之外,您可能还想对服务器执行其他操作,那么这很有意义,或者如果你真的想构建它,做其他事情,然后 运行 它。
但如果您希望调用者总是执行 Server server; server.run()
,那么也许您甚至不需要 class -- 它可以是一个 stand-alone 函数 run_server()
。如果你一开始就没有状态来封装和传递,那么你不一定需要对象。 stand-alone 函数甚至可以标记为 [[noreturn]]
以向用户和编译器表明该函数从不 returns.
如果不进一步了解您的用例,很难说哪个更有意义。但简而言之:构造函数构造对象——如果您正在做其他事情,请不要为此使用它们。
大多数情况下,你的代码是没有问题的。由于以下规则:
A class is considered a completely-defined object type ([basic.types]) (or complete type) at the closing } of the class-specifier. Within the class member-specification, the class is regarded as complete within function bodies, default arguments, noexcept-specifiers, and default member initializers (including such things in nested classes). Otherwise it is regarded as incomplete within its own class member-specification.
但是,对您的代码的一个限制是您不能使用不是从指针 this
获得的左值来访问此对象,因为行为未指定。它受此规则约束:
During the construction of an object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor's this pointer, the value of the object or subobject thus obtained is unspecified.
此外,您不能使用实用程序 shared_ptr
来管理此类 class 对象。通常,将无限循环放入构造函数中并不是一个好主意。使用对象时会有很多限制。
假设我有一个在无限循环中提供某种功能的对象。
将无限循环放在构造函数中是否可以接受?
示例:
class Server {
public:
Server() {
for(;;) {
//...
}
}
};
或者如果构造函数从未完成,C++ 是否存在固有的初始化问题?
(想法是,对于 运行 一个服务器,你只需说 Server server;
,可能在某处的线程中...)
这是允许的。但与任何其他无限循环一样,它 must have observable side effects, otherwise you get undefined behavior.
调用网络函数算作“可观察到的副作用”,所以你是安全的。此规则仅禁止那些实际上什么都不做或只是在不与外界交互的情况下随机播放数据的循环。
按照标准并没有错,只是设计不好。
构造函数通常不会阻塞。它们的目的是获取原始内存块,并将其转换为有效的 C++ 对象。析构函数恰恰相反:它们获取有效的 C++ 对象并将它们转回原始内存块。
如果你的构造函数永远阻塞(强调永远),它所做的事情不同于仅仅将一块内存变成一个对象。 可以短时间阻塞(互斥锁就是一个很好的例子),如果这有助于对象的构造。 在您的情况下,看起来您的构造函数正在接受和服务客户。这不是把内存变成对象。
我建议您将构造函数拆分为一个构建服务器对象的“真实”构造函数和另一个为客户端提供服务的 start
方法(通过启动事件循环)。
ps:在某些情况下,您 必须 与构造函数分开执行对象的 functionality/logic,例如,如果您的 class 继承自 std::enable_shared_from_this
.
它是合法的,但最好避免它。
主要问题是您应该避免让用户感到意外。拥有一个从不 returns 的构造函数是不寻常的,因为它不合逻辑。你为什么要构建你永远无法使用的东西?因此,虽然该模式可能有效,但它不太可能是预期的行为。
第二个问题是它限制了您的服务器 class 的使用方式。 C++ 的构造和破坏过程是该语言的基础,因此劫持它们可能很棘手。例如,一个人可能希望 Server
是 class 的成员,但现在总体 class' 构造函数将阻塞......即使这不直观。这也使得将这些对象放入容器变得非常困难,因为这可能涉及分配许多对象。
我能想到的最接近你所做的是std::thread
。 Thread 不会永远阻塞,但它确实有一个构造函数可以完成大量工作。但是如果你看看 std::thread
,你就会意识到,当谈到多线程时,惊讶是常态,所以人们在这样的选择上的麻烦就更少了。 (我个人并不清楚在构造时启动线程的原因,但是多线程中有很多极端情况,如果它解决了其中一些我不会感到惊讶)
用户可能希望在主线程中设置您的 Server
对象。然后在工作线程中调用 server.endless_loop()
函数。
在实际服务器中,获取端口的过程需要升级权限,然后可以删除。或者您有一个需要加载设置的对象。在其他地方发生长期循环之前,这些任务可能发生在主线程中。
就个人而言,我更希望您的对象具有快速且非阻塞的“轮询”功能。然后你可以有一个循环函数,在无限循环中调用 poll 和 sleep 。您甚至可能有一个原子变量,您可以将其设置为从不同的线程退出循环。另一个功能是在服务器对象中启动一个内部线程。
正如其他人所指出的,就 C++ 语义而言,这并没有什么“错误”,但它的设计很糟糕。构造函数的目的是构造一个对象,因此如果该任务永远不会完成,那么用户将感到惊讶。
其他人提出了关于将构造和 运行 步骤拆分为构造函数和方法的建议,如果除了 运行 它之外,您可能还想对服务器执行其他操作,那么这很有意义,或者如果你真的想构建它,做其他事情,然后 运行 它。
但如果您希望调用者总是执行 Server server; server.run()
,那么也许您甚至不需要 class -- 它可以是一个 stand-alone 函数 run_server()
。如果你一开始就没有状态来封装和传递,那么你不一定需要对象。 stand-alone 函数甚至可以标记为 [[noreturn]]
以向用户和编译器表明该函数从不 returns.
如果不进一步了解您的用例,很难说哪个更有意义。但简而言之:构造函数构造对象——如果您正在做其他事情,请不要为此使用它们。
大多数情况下,你的代码是没有问题的。由于以下规则:
A class is considered a completely-defined object type ([basic.types]) (or complete type) at the closing } of the class-specifier. Within the class member-specification, the class is regarded as complete within function bodies, default arguments, noexcept-specifiers, and default member initializers (including such things in nested classes). Otherwise it is regarded as incomplete within its own class member-specification.
但是,对您的代码的一个限制是您不能使用不是从指针 this
获得的左值来访问此对象,因为行为未指定。它受此规则约束:
During the construction of an object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor's this pointer, the value of the object or subobject thus obtained is unspecified.
此外,您不能使用实用程序 shared_ptr
来管理此类 class 对象。通常,将无限循环放入构造函数中并不是一个好主意。使用对象时会有很多限制。