类构造函数可以永远阻塞吗?

Cap*_*man 42 c++ multithreading constructor infinite-loop

假设我有一个在无限循环中提供某种功能的对象。

将无限循环放在构造函数中是否可以接受?

例子:

class Server {
    public:
    Server() {
        for(;;) {
            //...
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

或者如果构造函数永远不会完成,C++ 中是否存在固有的初始化问题?

(这个想法是运行你刚才说的服务器Server server;,可能在某个线程中......)

Dav*_*aim 76

根据标准,这并没有错,这只是一个糟糕的设计。

构造函数通常不会阻塞。它们的目的是获取原始内存块,并将其转换为有效的 C++ 对象。析构函数做相反的事情:它们获取有效的 C++ 对象并将它们转换回原始内存块。

如果你的构造函数永远阻塞(强调永远),它会做一些不同的事情,而不仅仅是将一块内存变成一个对象。如果这服务于对象的构造,则可以短时间阻塞(互斥锁就是一个很好的例子)。在您的情况下,看起来您的构造函数正在接受和服务客户​​。这并不是将内存变成对象。

我建议您将构造函数拆分为构建服务器对象的“真实”构造函数和start为客户端提供服务的另一个方法(通过启动事件循环)。

ps:在某些情况下,您必须与构造函数分开执行对象的功能/逻辑,例如,如果您的类继承自std::enable_shared_from_this.

  • @PaulDraper:强调_永远_。阻塞获取资源与永远阻塞是非常不同的。 (7认同)
  • 总的来说,我同意你的观点,尽管现代构造函数似乎并不*总是*只是将内存转换为对象。例如,您的互斥锁示例就是一个,或者另一个示例是立即启动给定函数的 C++ 线程构造函数。有人可能会说创建服务器是构造函数的角色...... (4认同)
  • 有几件事:1) `std::thread` 背后有一些错误的决定。这就是“std::jthread”被标准化的原因。2) 我实际上更希望线程对象有一个启动方法 3) 确实,有时我们会滥用自己的原则。软件工程原理是经验法则——通常它们对我们有帮助。我们很少打破它们,因为它们阻碍我们做一些更简单、更正确的事情。 (2认同)
  • 打开文件是您可以在 RAII 构造函数中合法执行的操作;可能会阻塞路径名查找的 I/O,在负载较重的系统上可能会阻塞数十毫秒或数百毫秒。或者整整几秒钟来旋转闲置的磁力驱动器。(或者 NFS 在火星上挂载服务器...) (2认同)
  • @CaptainCodeman 是正确的:RAII 说构造函数应该不仅仅是内存操作。 (2认同)

Hol*_*Cat 18

这是允许的。但与任何其他无限循环一样,它必须具有可观察到的副作用,否则会出现未定义的行为

调用网络函数算作“可观察到的副作用”,所以你很安全。该规则仅禁止循环,这些循环要么实际上什么都不做,要么只是在不与外部世界交互的情况下混洗数据。

  • @CaptainCodeman 未定义的行为。https://en.cppreference.com/w/cpp/language/ub 这就是标准所说的当您违反语言的某些规则时会发生的各种不可预测的行为(任何事情都可能发生;包括按预期工作的代码,可能直到一些不相关的事情发生)事情发生变化)。 (7认同)

Cor*_*ica 16

它是合法的,但避免它是一个好主意。

主要问题是您应该避免让用户感到惊讶。有一个从不返回的构造函数是不寻常的,因为它不合逻辑。你为什么要建造一些你永远无法使用的东西?因此,虽然该模式可能有效,但它不太可能是预期的行为。

第二个问题是它限制了您的 Server 类的使用方式。C++ 的构造和销毁过程是该语言的基础,因此劫持它们可能很棘手。例如,一个人可能想要一个Server是类的成员,但现在总体类的构造函数将阻塞......即使这不直观。这也使得将这些对象放入容器变得非常困难,因为这可能涉及分配许多对象。

我能想到的最接近你正在做的事情是std::thread. Thread 不会永远阻塞,但它确实有一个构造函数可以完成惊人的大量工作。但是,如果您查看std::thread,您就会意识到当涉及到多线程时,感到惊讶是常态,因此人们对此类选择的麻烦较少。(我个人不知道在构造时启动线程的原因,但是多线程中有很多极端情况,如果它解决了其中的一些问题,我不会感到惊讶)