如何在创建对象时使用 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 或智能指针都有助于使您的构造函数异常安全。