假设我有一个对象,在无限循环中提供某种功能。
将无限循环放在构造函数中是否可行?
例如:
class Server {
public:
Server() {
for(;;) {
//...
}
}
};
如果构造函数从未完成,C++中是否存在固有的初始化问题?
(意思是运行服务器只需在某个线程中键入Server server;
即可...)
假设我有一个对象,在无限循环中提供某种功能。
将无限循环放在构造函数中是否可行?
例如:
class Server {
public:
Server() {
for(;;) {
//...
}
}
};
如果构造函数从未完成,C++中是否存在固有的初始化问题?
(意思是运行服务器只需在某个线程中键入Server server;
即可...)
这不是按照标准做法,而是一种不好的设计。
构造函数通常不会阻塞。它们的目的是将一块原始内存转换为有效的C++对象。析构函数则相反:它们将有效的C++对象转换回原始内存块。
如果你的构造函数永远(强调永远)阻塞,它所做的事情就不同于只是将一块内存转换成对象了。 如果阻塞时间很短(互斥锁就是一个很好的例子),以服务于对象的构造,则可以阻塞。 在你的情况下,看起来你的构造函数正在接受和为客户端提供服务。这不是将内存转换为对象。
我建议你将构造函数拆分为一个“真正的”构造函数,用于构建服务器对象,再添加另外一个start
方法来为客户端提供服务(通过启动事件循环)。
注:在某些情况下,必须从构造函数中单独执行对象的功能/逻辑,例如如果你的类继承自std::enable_shared_from_this
。
std::thread
背后有一些不好的决策。这就是为什么std::jthread
被标准化的原因。2)我实际上更倾向于线程对象具有启动方法3)的确,有时我们会滥用我们自己的原则。软件工程原则是经验法则-通常它们帮助我们。很少情况下,我们打破它们,因为它们阻止我们做一些更简单、更正确的事情。 - David Haim是允许这样做的。但像其他无限循环一样,它必须具有可观察的副作用,否则你将得到未定义行为。
调用网络功能被视为“可观察的副作用”,因此您是安全的。此规则仅禁止那些真正不执行任何操作或只是在数据之间移动而不与外部世界交互的循环。
这是合法的,但最好避免使用。
主要问题在于应该避免惊讶用户。拥有一个永远不会返回的构造函数是不合逻辑的,因此构造一个永远不能使用的东西是不寻常的。虽然这种模式可能有效,但不太可能符合预期行为。
第二个问题是它限制了如何使用你的Server类。C++的构造和析构过程是语言的基础,因此劫持它们可能会很棘手。例如,一个人可能想要一个Server
作为类的成员,但现在那个超越的类的构造函数将阻塞......即使这并不直观。这也使得将这些对象放入容器中变得非常困难,因为这可能涉及到分配许多对象。
我能想到的与您所做的最接近的是std::thread
。线程并不永远阻塞,但它确实有一个构造函数,执行的工作量相当大。但如果看一下std::thread
,就会意识到,在多线程方面,被惊讶是正常的,因此人们对这样的选择没有太大问题。(我个人不知道启动线程时构造函数的原因,但在多线程中有这么多的边角情况,如果它解决了其中一些问题,我不会感到惊讶)
用户可能期望在主线程中设置您的Server
对象。然后在工作线程中调用server.endless_loop()
函数。
在实际服务器中,获取端口的过程需要提升特权,然后可以放弃。或者您有一个需要加载设置的对象。这些任务可以在长期循环发生之前在主线程中进行。
个人而言,我更喜欢您的对象拥有一个快速且非阻塞的“poll”函数。然后您可以有一个循环函数,在其中调用poll和sleep,形成无限循环。您甚至可以使用原子变量来设置从不同线程退出循环的方式。另一个功能是在Server对象内部启动内部线程。
Server server; server.run()
,那么您甚至不需要一个类——它可以是一个独立的函数run_server()
。如果您没有要封装和传递的状态,则不一定需要对象。一个独立的函数甚至可以被标记为[[noreturn]]
,以使用户和编译器清楚地知道该函数永远不会返回。
server::run
函数或类似的函数中放置循环呢? - PeterServer server;
创建并运行服务器。Server server; server.run();
更易读且更易维护。 - rustyx