在 C++ 中从构造函数中显式调用析构函数是不好的做法吗?

Tib*_*tim 3 c++ constructor destructor exception class

我通常不会明确调用析构函数。但我正在设计 TCP 服务器类,它看起来像这样:

class Server {
public:
    Server() {
        try {
            WSADATA wsaData;
            if (WSAStartup(MAKEWORD(2, 2), &wsaData))
                throw std::runtime_error("WSAStartup function failed.");
            ...

            if ((m_scListener = socket(pAddr->ai_family, pAddr->ai_socktype, pAddr->ai_protocol)) == INVALID_SOCKET)
                throw std::runtime_error("'socket' function failed.");
            ...
        }
        catch (std::exception& ex) {
            this->~Server();
            throw;
        }
    }

    ~Server() {
        if (m_scListener != INVALID_SOCKET) {
            closesocket(m_scListener);
            m_scListener = INVALID_SOCKET;
        }
        WSACleanup();
    }
private:
    SOCKET m_scListener = INVALID_SOCKET;
}
Run Code Online (Sandbox Code Playgroud)

上面的代码是否被认为是不好的做法或设计?什么是推荐的设计方式?我是这样写的,因为构造函数不能返回NULL。我应该将构造函数设为私有,并编写创建 Server 类实例的静态方法吗?

===== 更新 =====

OK,总结一下答案,我得出了这个结论:

  • 显式调用析构函数通常是一个坏主意,即使它按预期工作,这是不寻常的,并且将处理您的代码的其他 C++ 程序员可能会对这种方法感到困惑。所以最好避免显式调用析构函数。

  • 将我原来的 RAII 类分解为微型 RAII 类看起来是一个很好的解决方案。但恐怕我的真实代码中有太多 API 调用需要清理(closesocket、CloseHandle、DeleteCriticalSection 等...)。其中一些只调用一次并且永远不会重用,将它们全部移到单独的 RAII 类中对我来说似乎太狂热了。这也会增加我的代码。

  • 在我看来,最有帮助的答案来自MM

更好的解决方案是将初始化代码保留在构造函数中,并在抛出之前调用清理函数。

按照 MM 的建议,我以这种方式重写了我的代码:

class Server {
public:
    Server() {
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2, 2), &wsaData))
            ThrowError("WSAStartup function failed.", true);
        ...

        if ((m_scListener = socket(pAddr->ai_family, pAddr->ai_socktype, pAddr->ai_protocol)) == INVALID_SOCKET)
            ThrowError("'socket' function failed.", true);
        ...
    }

    ~Server() { CleanUp(); }

private:
    SOCKET m_scListener = INVALID_SOCKET;

    void ThrowError(const char* error, bool cleanUp) {
        if (cleanUp)
            CleanUp();
        throw std::runtime_error(error);
    }

    void CleanUp() {
        if (m_scListener != INVALID_SOCKET) {
            closesocket(m_scListener);
            m_scListener = INVALID_SOCKET;
        }
        WSACleanup();
    }
};
Run Code Online (Sandbox Code Playgroud)

我相信这个设计遵循 RAII 模式,但只有一个类,而不是 3-4 个微型 RAII 类。

eer*_*ika 5

在 C++ 中从构造函数中显式调用析构函数是不好的做法吗?

是的。如果调用尚未构造的对象的析构函数,则程序的行为是未定义的。

有未定义的行为是一件坏事。应尽可能避免。


什么是推荐的设计方式?

遵循单一职责原则 (SRP) 和资源获取即初始化 (RAII) 模式。

尤其是你的Server责任太多。您应该创建一个单独的类来管理套接字。在这一类中,调用构造函数scoket和析构函数中,调用的类,调用 closesocket。保持包含的套接字始终有效(可关闭)或INVALID_SOCKET始终唯一(如果有效且永远不会泄漏(即,在未先关闭的情况下永远不会覆盖该值)。这是 RAII 模式。

为 wsa 数据创建一个类似的包装器。

在 中Server,存储这些包装器类型的成员。Server然后不需要自定义析构函数或其他特殊成员函数,因为它们由管理自己的成员处理。