在 C++ 中显式地从构造函数中调用析构函数是不好的做法吗?
Is explicitly calling destructors from constructors bad practice in C++?
我通常不会显式调用析构函数。但我正在设计 TCP 服务器 class,它看起来像这样:
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;
}
上面的代码是不好的做法还是设计?推荐的设计方法是什么?我是这样写的,因为构造函数不能return NULL。我是否应该将构造函数设为私有,并编写创建服务器实例的静态方法 class?
===== 更新 =====
OK,总结一下答案,我得出了这样的结论:
显式调用析构函数通常不是一个好主意,即使它按预期工作,这也是不寻常的,并且将处理您的代码的其他 C++ 程序员可能会对这种方法感到困惑。所以最好避免显式调用析构函数。
将我原来的 RAII class 分解成微型 RAII classes 看起来是个不错的解决方案。但我担心我的真实代码中有太多 API 调用需要清理(closesocket、CloseHandle、DeleteCriticalSection 等...)。其中一些只被调用一次并且永远不会被重用,并且将它们全部移动到单独的 RAII classes 中对我来说似乎太狂热了。这样也会增加我的代码。
我认为最有帮助的答案来自 M.M:
A better solution would be to keep the initialization code in the
constructor, and call the cleanup function before throwing out.
按照 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 模式,但只有一个 class 而不是 3-4 个微型 RAII classes。
我不知道在技术层面上会发生什么,但看起来不太好。我建议不要那样做。 error-prone IMO 在 class 的单独 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;
};
Caller-side代码:
Server server;
if (!server.init()) {
server.CleanUp();
}
Is the code above considered as bad practice or design?
是的,显式调用构造函数或析构函数几乎总是错误的,除了极少数情况,这不是一个。
What's recommended way of designing this?
推荐的方式是使用RAII。在这种情况下,您可以将 std::unique_ptr
与调用 closesocket()
等的自定义删除器一起使用。或者您可以创建自己的包装器。然后您可以安全地抛出异常并确保正确清理初始化的资源。
Is explicitly calling destructors from constructors bad practice in C++?
是的。如果调用尚未构造的对象的析构函数,程序的行为是未定义的。
有未定义的行为是一件坏事。应尽可能避免。
What's recommended way of designing this?
遵循单一职责原则 (SRP) 和资源获取即初始化 (RAII) 模式。
尤其是你的Server
责任太多了。您应该创建一个单独的 class 来管理套接字。在 class 的构造函数中,调用 scoket
,在析构函数中,调用 that class,调用 closesocket
。保持 class 不变量,即所包含的套接字始终有效(可关闭)或 INVALID_SOCKET
并且如果有效且永远是唯一的并且永远不会泄漏(即该值永远不会在不先关闭的情况下被覆盖)。这是RAII模式。
为 wsa 数据创建一个类似的包装器。
在 Server
中,存储这些包装器类型的成员。 Server
将不再需要自定义析构函数或其他特殊成员函数,因为它们由自行管理的成员处理。
看看你的设计,在构造函数的 socket()
调用中有这段代码:
pAddr->ai_family, pAddr->ai_socktype, pAddr->ai_protocol
.
如果 Server
class 的用户想要使用不同的套接字类型、协议等怎么办?在 之前 socket()
是开了?他们没有追索权,因为他们被锁定在您在 pAddr
中使用的值中(您从未提到您从哪里获得这些值,但它们肯定是在 Server
构造函数之前或在构造函数中设置的)。
如果您将这些套接字参数设为 class 的独立成员,这将打开 class 设计,因此无需调用 ill-conceived 析构函数,因为构造函数不会参与调用 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
的参数是初始化为基本值的成员变量,但可以用set...
函数改变before打电话给 Server::start()
.
另一个添加是 Server
的析构函数中的 try/catch
。请注意,这样做是为了确保可以抛出的任何东西都不会逃脱析构函数调用,否则将调用 std::terminate
。
What's recommended way of designing this?
我会说:更多 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;
};
我通常不会显式调用析构函数。但我正在设计 TCP 服务器 class,它看起来像这样:
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;
}
上面的代码是不好的做法还是设计?推荐的设计方法是什么?我是这样写的,因为构造函数不能return NULL。我是否应该将构造函数设为私有,并编写创建服务器实例的静态方法 class?
===== 更新 =====
OK,总结一下答案,我得出了这样的结论:
显式调用析构函数通常不是一个好主意,即使它按预期工作,这也是不寻常的,并且将处理您的代码的其他 C++ 程序员可能会对这种方法感到困惑。所以最好避免显式调用析构函数。
将我原来的 RAII class 分解成微型 RAII classes 看起来是个不错的解决方案。但我担心我的真实代码中有太多 API 调用需要清理(closesocket、CloseHandle、DeleteCriticalSection 等...)。其中一些只被调用一次并且永远不会被重用,并且将它们全部移动到单独的 RAII classes 中对我来说似乎太狂热了。这样也会增加我的代码。
我认为最有帮助的答案来自 M.M:
A better solution would be to keep the initialization code in the constructor, and call the cleanup function before throwing out.
按照 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 模式,但只有一个 class 而不是 3-4 个微型 RAII classes。
我不知道在技术层面上会发生什么,但看起来不太好。我建议不要那样做。 error-prone IMO 在 class 的单独 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;
};
Caller-side代码:
Server server;
if (!server.init()) {
server.CleanUp();
}
Is the code above considered as bad practice or design?
是的,显式调用构造函数或析构函数几乎总是错误的,除了极少数情况,这不是一个。
What's recommended way of designing this?
推荐的方式是使用RAII。在这种情况下,您可以将 std::unique_ptr
与调用 closesocket()
等的自定义删除器一起使用。或者您可以创建自己的包装器。然后您可以安全地抛出异常并确保正确清理初始化的资源。
Is explicitly calling destructors from constructors bad practice in C++?
是的。如果调用尚未构造的对象的析构函数,程序的行为是未定义的。
有未定义的行为是一件坏事。应尽可能避免。
What's recommended way of designing this?
遵循单一职责原则 (SRP) 和资源获取即初始化 (RAII) 模式。
尤其是你的Server
责任太多了。您应该创建一个单独的 class 来管理套接字。在 class 的构造函数中,调用 scoket
,在析构函数中,调用 that class,调用 closesocket
。保持 class 不变量,即所包含的套接字始终有效(可关闭)或 INVALID_SOCKET
并且如果有效且永远是唯一的并且永远不会泄漏(即该值永远不会在不先关闭的情况下被覆盖)。这是RAII模式。
为 wsa 数据创建一个类似的包装器。
在 Server
中,存储这些包装器类型的成员。 Server
将不再需要自定义析构函数或其他特殊成员函数,因为它们由自行管理的成员处理。
看看你的设计,在构造函数的 socket()
调用中有这段代码:
pAddr->ai_family, pAddr->ai_socktype, pAddr->ai_protocol
.
如果 Server
class 的用户想要使用不同的套接字类型、协议等怎么办?在 之前 socket()
是开了?他们没有追索权,因为他们被锁定在您在 pAddr
中使用的值中(您从未提到您从哪里获得这些值,但它们肯定是在 Server
构造函数之前或在构造函数中设置的)。
如果您将这些套接字参数设为 class 的独立成员,这将打开 class 设计,因此无需调用 ill-conceived 析构函数,因为构造函数不会参与调用 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
的参数是初始化为基本值的成员变量,但可以用set...
函数改变before打电话给 Server::start()
.
另一个添加是 Server
的析构函数中的 try/catch
。请注意,这样做是为了确保可以抛出的任何东西都不会逃脱析构函数调用,否则将调用 std::terminate
。
What's recommended way of designing this?
我会说:更多 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;
};