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

时间:2020-11-06 03:49:07

标签: c++ class exception constructor destructor

我通常不明确地调用析构函数。但是我正在设计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;
}

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

===== U P D A T E =====

好,总结一下答案,我得出以下结论:

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

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

  • 我认为最有帮助的答案来自 MM

更好的解决方案是将初始化代码保留在 构造函数,并在丢弃之前调用cleanup函数。

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

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();
    }
};

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

6 个答案:

答案 0 :(得分:5)

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

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

具有不确定的行为是一件坏事。应尽可能避免使用这种方式。


推荐的设计方式是什么?

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

尤其是您的Server负有太多责任。您应该创建一个单独的类来管理套接字。在该类的构造函数中,调用scoket,在析构函数中,调用 that 类,并调用closesocket。保持类不变,以确保所包含的套接字始终有效(可关闭)或INVALID_SOCKET,并且在有效时始终唯一并且永不泄漏(即,值必须在不先关闭的情况下才被覆盖)。这是RAII模式。

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

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

答案 1 :(得分:4)

析构函数只能由完全构造的对象调用。

您可以制作一个Init()和CleanUp()函数,而不是将设置代码放入构造函数中。这还将使您的Server对象的构建速度更快。

class Server {
public:
    Server() = default;

    bool Init() {
      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.");
            ...
            return true;
        }
        catch (std::exception& ex) {
            return false;
        }
    }

    void CleanUp() {
        if (m_scListener != INVALID_SOCKET) {
            closesocket(m_scListener);
            m_scListener = INVALID_SOCKET;
        }
        WSACleanup();
    }

    ~Server() {
      CleanUp();
    }

private:
    SOCKET m_scListener = INVALID_SOCKET;
};

主叫方代码:

Server server;
if (!server.init()) {
   server.CleanUp();
}

答案 2 :(得分:3)

推荐的设计方式是什么?

我会说:更多的RAII。像这样:

class WSARaii
{
public:
    WSARaii()
    {
        if (WSAStartup(MAKEWORD(2, 2), &wsaData))
            throw std::runtime_error("WSAStartup function failed.");
    }
    ~WSARaii()
    {
        WSACleanup();
    }
    WSARaii(const WSARaii&) = delete;
    WSARaii& operator =(const WSARaii&) = delete;

private:
    WSADATA wsaData;
};

class Socket
{
public:
    Socket(..) : m_scListener(socket(pAddr->ai_family, pAddr->ai_socktype, pAddr->ai_protocol) {
        if (m_scListener == INVALID_SOCKET)
            throw std::runtime_error("'socket' function failed.");
    }
    ~Server() {
        if (m_scListener != INVALID_SOCKET) {
            closesocket(m_scListener);
        }
    }
private:
    SOCKET m_scListener
};

最后

class Server {
public:
    Server() : wsa(), socket(..) {}

private:
    WSARaii wsa;
    Socket socket;
};

答案 3 :(得分:1)

我不知道在技术层面上会发生什么,但是看起来并不好。我建议不要这样做。在类中的一个单独的Init()方法内初始化诸如网络之类的高级系统要容易得多,并且不易出错,IMO则更容易。这样,您可以安全地创建实例,调用其Init()方法,检查结果,并在失败时delete(或调用Destroy(),或同时调用两者)。

我只会在构造函数内部分配默认值,并让外部代码使用delete来调用析构函数。

答案 4 :(得分:1)

上面的代码是否被视为不良做法或设计?

是的,显式地调用构造函数或析构函数几乎总是错误的,除了极少数情况之外,并非如此。

推荐的设计方式是什么?

推荐的方式是使用RAII。在这种情况下,您可以将std::unique_ptr与调用closesocket()等的自定义删除器一起使用。也可以创建自己的包装器。然后,您可以放心地引发异常,并确保正确初始化了初始化的资源。

答案 5 :(得分:1)

看看您的设计,在构造函数的socket()调用中有以下代码:

pAddr->ai_family, pAddr->ai_socktype, pAddr->ai_protocol

如果Server类的用户想使用不同的套接字类型,协议等。之前,打开socket()?它们没有资源,因为它们被锁定在pAddr中使用的值中(您从未提到要在何处获取这些值,但是它们肯定是在Server构造函数中或内部设置的)。

如果您将这些套接字参数设置为类的单个成员,则会打开类设计,从而无需调用对析构函数的错误构想的调用,因为构造函数不会参与调用{{1 }}甚至socket()

WSAStartup

(对我而言)这是一个更简洁,更灵活的接口,不需要显式调用析构函数。仅在class Server { public: void set_family(int family) { m_family = family; } //.. other setters void start() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData)) throw std::runtime_error("WSAStartup function failed."); if ((m_scListener = socket(m_family, m_type, m_protocol)) == INVALID_SOCKET) throw std::runtime_error("'socket' function failed."); } void stop() { if (m_scListener != INVALID_SOCKET) { closesocket(m_scListener); m_scListener = INVALID_SOCKET; } WSACleanup(); } ~Server() noexcept { try { stop(); } catch(...) { } // add any additional catches above this, // but make sure no exceptions escape the destructor } private: SOCKET m_scListener = INVALID_SOCKET; int m_family = AF_INET; int m_type = SOCK_STREAM; int m_protocol = IPPROTO_TCP; }; 调用中完成WinSock的实际初始化以及与套接字的连接。

此外,start()的参数是已初始化为基本值的成员变量,但可以在调用{{1}之前使用socket函数进行更改。 }}。

另一个附加项是set...的析构函数中的Server::start()。请注意,这样做是为了确保可以抛出的任何内容都不会逃脱析构函数调用,否则将调用try/catch

相关问题