如何在创建对象时使用 std::optional 进行错误处理而不立即销毁它们?
How to use std::optional for error handling when creating objects without instantly destructing them?
错误处理是 C++ 构造函数中的一个挑战。有几种常见的方法,但它们都有明显的缺点。例如,抛出异常可能会导致构造函数中较早分配的资源泄漏,使其成为一种容易出错的方法。使用静态 init()
方法是另一种常见的解决方案,但它违背了 RAII 原则。
研究主题时我发现 this answer and blog 建议使用名为 std::optional<>
的 C++17 功能,我发现它很有前途。然而,这种解决方案似乎有一个潜在的问题——当用户检索到对象时,它会立即触发析构函数。
这里有一个描述问题的简单代码示例,我的代码是基于上述来源
class A
{
public:
A(int myNum);
~A();
static std::optional<A> make(int myNum);
bool isBuf() { return _buf; };
private:
char* _buf;
};
std::optional<A> A::make(int myNum)
{
std::cout << "A::make()\n";
if (myNum < 8)
return {};
return A(myNum);
}
A::A(int myNum)
{
std::cout << "A()\n";
_buf = new char[myNum];
}
A::~A()
{
std::cout << "~A()\n";
delete[]_buf;
}
int main()
{
if (std::optional<A> a = A::make(42))
{
if (a->isBuf())
std::cout << "OK\n";
else
std::cout << "NOT OK\n";
std::cout << "if() finished\n";
}
std::cout << "main finished\n";
}
这个程序的输出将是:
A::make()
A()
~A()
OK
if() finished
~A()
随后出现运行时错误(至少在 Visual C++ 环境中)试图删除 a->_buf
两次。
为了 reader 的方便,我使用了 cout
,因为我在调试非常复杂的代码时发现了这个问题,但问题很明显 - return
中的语句 [=18] =] 构造对象,但由于它是 A::make()
范围的末尾 - 调用析构函数。用户确定他的对象已初始化(注意我们如何获得 "OK"
消息),而实际上它已被销毁,当我们走出 main
、[=23 中的 if()
范围时=] 再次被调用。
那么,我做错了吗?
在构造函数中使用 std::optional
进行错误处理很常见,至少有人告诉我。提前致谢
您的 class 违反了 rule of 3/5。
检测复制构造函数并简化 main
得到:
#include <optional>
#include <iostream>
class A
{
public:
A(int myNum);
~A();
A(const A& other){
std::cout << "COPY!\n";
}
static std::optional<A> make(int myNum);
bool isBuf() { return _buf; };
private:
char* _buf = nullptr;
};
std::optional<A> A::make(int myNum)
{
std::cout << "A::make()\n";
if (myNum < 8)
return {};
return A(myNum);
}
A::A(int myNum)
{
std::cout << "A()\n";
_buf = new char[myNum];
}
A::~A()
{
std::cout << "~A()\n";
delete[]_buf;
}
int main()
{
std::optional<A> a = A::make(42);
std::cout << "main finished\n";
}
输出为:
A::make()
A()
COPY!
~A()
main finished
~A()
当您调用 A::make()
时,本地 A(myNum)
被复制到返回的 optional
中,然后调用其析构函数。如果没有 std::optional
(例如,按值返回 A
),您会遇到同样的问题。
我添加的复制构造函数不复制任何内容,但编译器生成的复制构造函数确实对 char* _buf;
成员进行了浅表复制。由于您没有正确地深度复制缓冲区,它会被删除两次,从而导致运行时错误。
对 0 规则使用 std::vector
,或正确执行 3/5 规则。您的代码调用了未定义的行为。
PS 与问题没有直接关系,但你应该初始化成员而不是在构造函数体中分配给它们。变化:
A::A(int myNum)
{
std::cout << "A()\n";
_buf = new char[myNum];
}
至
A::A(int myNum) : _buf( new char[myNum])
{
std::cout << "A()\n";
}
或者更好的是,使用上面提到的 std::vector
。
PPS:
Throwing exceptions for example, may cause leak of the allocated resources earlier in the constructor, making it an error prone approach.
不,从构造函数中抛出是很常见的,当您不通过原始指针管理内存时也没有问题。使用 std::vector
或智能指针都有助于使您的构造函数异常安全。
错误处理是 C++ 构造函数中的一个挑战。有几种常见的方法,但它们都有明显的缺点。例如,抛出异常可能会导致构造函数中较早分配的资源泄漏,使其成为一种容易出错的方法。使用静态 init()
方法是另一种常见的解决方案,但它违背了 RAII 原则。
研究主题时我发现 this answer and blog 建议使用名为 std::optional<>
的 C++17 功能,我发现它很有前途。然而,这种解决方案似乎有一个潜在的问题——当用户检索到对象时,它会立即触发析构函数。
这里有一个描述问题的简单代码示例,我的代码是基于上述来源
class A
{
public:
A(int myNum);
~A();
static std::optional<A> make(int myNum);
bool isBuf() { return _buf; };
private:
char* _buf;
};
std::optional<A> A::make(int myNum)
{
std::cout << "A::make()\n";
if (myNum < 8)
return {};
return A(myNum);
}
A::A(int myNum)
{
std::cout << "A()\n";
_buf = new char[myNum];
}
A::~A()
{
std::cout << "~A()\n";
delete[]_buf;
}
int main()
{
if (std::optional<A> a = A::make(42))
{
if (a->isBuf())
std::cout << "OK\n";
else
std::cout << "NOT OK\n";
std::cout << "if() finished\n";
}
std::cout << "main finished\n";
}
这个程序的输出将是:
A::make()
A()
~A()
OK
if() finished
~A()
随后出现运行时错误(至少在 Visual C++ 环境中)试图删除 a->_buf
两次。
为了 reader 的方便,我使用了 cout
,因为我在调试非常复杂的代码时发现了这个问题,但问题很明显 - return
中的语句 [=18] =] 构造对象,但由于它是 A::make()
范围的末尾 - 调用析构函数。用户确定他的对象已初始化(注意我们如何获得 "OK"
消息),而实际上它已被销毁,当我们走出 main
、[=23 中的 if()
范围时=] 再次被调用。
那么,我做错了吗?
在构造函数中使用 std::optional
进行错误处理很常见,至少有人告诉我。提前致谢
您的 class 违反了 rule of 3/5。
检测复制构造函数并简化 main
得到:
#include <optional>
#include <iostream>
class A
{
public:
A(int myNum);
~A();
A(const A& other){
std::cout << "COPY!\n";
}
static std::optional<A> make(int myNum);
bool isBuf() { return _buf; };
private:
char* _buf = nullptr;
};
std::optional<A> A::make(int myNum)
{
std::cout << "A::make()\n";
if (myNum < 8)
return {};
return A(myNum);
}
A::A(int myNum)
{
std::cout << "A()\n";
_buf = new char[myNum];
}
A::~A()
{
std::cout << "~A()\n";
delete[]_buf;
}
int main()
{
std::optional<A> a = A::make(42);
std::cout << "main finished\n";
}
输出为:
A::make()
A()
COPY!
~A()
main finished
~A()
当您调用 A::make()
时,本地 A(myNum)
被复制到返回的 optional
中,然后调用其析构函数。如果没有 std::optional
(例如,按值返回 A
),您会遇到同样的问题。
我添加的复制构造函数不复制任何内容,但编译器生成的复制构造函数确实对 char* _buf;
成员进行了浅表复制。由于您没有正确地深度复制缓冲区,它会被删除两次,从而导致运行时错误。
对 0 规则使用 std::vector
,或正确执行 3/5 规则。您的代码调用了未定义的行为。
PS 与问题没有直接关系,但你应该初始化成员而不是在构造函数体中分配给它们。变化:
A::A(int myNum)
{
std::cout << "A()\n";
_buf = new char[myNum];
}
至
A::A(int myNum) : _buf( new char[myNum])
{
std::cout << "A()\n";
}
或者更好的是,使用上面提到的 std::vector
。
PPS:
Throwing exceptions for example, may cause leak of the allocated resources earlier in the constructor, making it an error prone approach.
不,从构造函数中抛出是很常见的,当您不通过原始指针管理内存时也没有问题。使用 std::vector
或智能指针都有助于使您的构造函数异常安全。