0

错误处理是 C++ 构造函数中的一个挑战。有几种常见的方法,但它们都有明显的缺点。例如,抛出异常可能会导致构造函数中更早分配的资源泄漏,使其成为一种容易出错的方法。使用静态init()方法是另一种常见的解决方案,但它违反了 RAII 原则。

研究这个主题,我发现这个答案和博客建议使用名为 的 C++17 功能std::optional<>,我发现它很有希望。然而,这种解决方案似乎有一个潜在的问题——当用户检索到对象时,它会立即触发析构函数。

这是一个描述问题的简单代码示例,我的代码基于上述来源

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

a->_buf尝试删除两次时出现运行时错误(至少在 Visual C++ 环境中) 。

cout为了读者的方便,我发现这个问题调试了一个非常复杂的代码,但问题很清楚 - 中的语句return构造A::make()对象,但由于它是A::make()范围的结尾 - 调用了析构函数。用户确定他的对象已初始化(注意我们是如何得到"OK"消息的),而实际上它已被销毁,并且当我们超出if()范围时maina->~A()再次调用。

那么,我做错了吗?在构造函数中使用std::optionalfor 错误处理很常见,至少有人告诉过我。提前致谢

4

1 回答 1

1

你的班级违反了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)将被复制到 retunred optional,然后调用其析构函数。std::optional如果没有(例如通过返回A按值),您也会遇到同样的问题。

我添加的复制构造函数不会复制任何内容,但生成的编译器会制作char* _buf;成员的浅表副本。由于您没有正确地深度复制缓冲区,因此它会被删除两次,从而导致运行时错误。

使用 astd::vector作为 0 规则,或正确执行 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上面提到的 a 。


PPS

例如,抛出异常可能会导致构造函数早期分配资源的泄漏,使其成为一种容易出错的方法。

不,从构造函数中抛出是很常见的,并且当您不通过原始指针管理内存时没有问题。使用 astd::vector或智能指针都将有助于使您的构造函数 excpetion 安全。

于 2020-12-05T18:25:09.817 回答