C++ 从构造函数显式调用析构函数在C+中是一种糟糕的做法+;?

C++ 从构造函数显式调用析构函数在C+中是一种糟糕的做法+;?,c++,class,exception,constructor,destructor,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

我通常不会显式调用析构函数。但是我正在设计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。我是否应该将构造函数设为私有,并编写创建服务器类实例的静态方法

==U p D A T E===

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

    > p>显式调用析构函数通常是个坏主意,即使它按预期的方式工作,这也是不寻常的,而其他C++程序员将处理代码,可能会与此方法混淆。所以最好避免显式调用析构函数


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

  • 在我看来,最有用的答案来自M.M

更好的解决方案是将初始化代码保存在 构造函数,并在抛出之前调用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级别。

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


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

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

您可以生成Init()和CleanUp()函数,而不是将设置代码放入构造函数中。这也将加快服务器对象的构造速度

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();
}
上述规范是否被视为不良实践或设计

是的,显式调用constructor或destructor几乎总是错误的,除了极少数情况,而这不是唯一的情况

推荐的设计方法是什么

推荐的方法是使用RAII。在这种情况下,您可以将
std::unique_ptr
与调用
closesocket()
等的自定义删除程序一起使用,也可以创建自己的包装器。然后您可以安全地抛出异常,并确保初始化的资源得到正确清理

明确地调用构造函数中的析构函数C++中的错误实践 对。如果调用尚未构造的对象的析构函数,则程序的行为未定义

行为不明确是件坏事。应尽可能避免这种情况


推荐的设计方法是什么

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

特别是,您的
服务器
承担了太多的责任。您应该创建一个单独的类来管理套接字。在该类的构造函数中,调用
scoket
,在析构函数中,调用该类的
closesocket
。保持类不变,即所包含的套接字始终有效(可关闭)或
无效\u socket
,如果有效,则始终唯一且从不泄漏(即,如果不先关闭,则永远不会覆盖该值)。这是RAII模式

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


Server
中,存储这些包装类型的成员<代码>服务器不需要自定义析构函数或其他特殊成员函数,因为这些函数是由管理自己的成员处理的。

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

pAddr->ai_系列,pAddr->ai_socktype,pAddr->ai_协议

如果
服务器
类的用户想要在打开
套接字()之前使用不同的套接字类型、协议等,该怎么办?它们没有追索权,因为它们被锁定在您在
pAddr
中使用的值中(您从未提到从何处获取这些值,但它们肯定是在
服务器
构造函数中设置的)

如果将这些套接字参数设置为类的单个成员,则会打开类设计,因此无需调用构思不周的析构函数调用,因为构造函数不会参与调用
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()
调用中完成

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

另一个添加项是
服务器
的析构函数中的
try/catch
。请注意,这样做是为了确保可以抛出的任何内容都不会逃逸析构函数调用,否则将调用
std::terminate

推荐的设计方法是什么

我和
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;
};