当我可以在析构函数中销毁对象时,为什么要使用 std::unique_ptr?

Why should I use an std::unique_ptr when I could just destroy the object in my destructor?

说我有这个 class:

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
    }

    ~Foo()
    {
        if(bar)
            delete bar;
    }
private:
    Bar* bar;

};

unique_ptr 臃肿时,为什么我要使用 std::unique_ptr 而不是原始指针?有什么优势吗?有没有我的析构函数不会被调用的情况?

你上面的代码实际上有一个错误,因为你没有定义复制构造函数或赋值运算符。想象一下这段代码:

Foo one;
Foo two = one;

因为 twoone 的副本,它使用默认的复制构造函数进行初始化 - 这使得两个 bar 指针指向同一个对象。这意味着当 two 的析构函数触发时,它将释放 one 共享的同一对象,因此 one 的析构函数将触发未定义的行为。哎呀

现在,如果您不想让您的对象可复制,您可以这样明确地说:

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
    }
    Foo(const Foo&) = delete;
    Foo& operator= (const Foo&) = delete;

    ~Foo()
    {
        if(bar)
            delete bar;
    }
private:
    Bar* bar;

};

这样就解决了这个问题 - 但看看涉及的代码量!您必须显式删除两个函数并手动编写析构函数。

除了还有一个问题。假设我这样做:

Foo one;
Foo two = std::move(one);

这通过将 one 的内容移动到 two 来初始化 two。或者是吗?不幸的是,答案是否定的,因为默认的移动构造函数将默认移动指针,它只是直接复制指针。所以现在你得到了和以前一样的东西。哎呀

不用担心!我们可以通过定义自定义移动构造函数和移动赋值运算符来解决此问题:

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
    }
    Foo(const Foo&) = delete;
    Foo& operator= (const Foo&) = delete;

    Foo(Foo&& rhs)
    {
        bar = rhs.bar;
        rhs.bar = nullptr;
    }

    Foo& operator= (Foo&& rhs)
    {
        if (bar != rhs.bar)
        {
            delete bar;
            bar = rhs.bar;
            rhs.bar = nullptr;
        }
    }

    ~Foo()
    {
        if(bar)
            delete bar;
    }
private:
    Bar* bar;

};

呸!这是一段 ton 的代码,但至少它是正确的。 (或者是吗?)

另一方面,假设您这样写:

class Foo
{
public:
    Foo() : bar(new Bar) {
    }
private:
    std::unique_ptr<Bar> bar;
};

哇,短多了!它自动确保 class 不能被复制, 它使默认移动构造函数和移动赋值运算符正常工作。

所以 std::unique_ptr 的一个巨大优势是它自动处理资源管理,是的,但另一个优势是它可以很好地处理复制和移动语义并且不会以意想不到的方式工作。这是使用它的主要原因之一。你可以说出你的意思 - "I'm the only one who should know about this thing, and you can't share it" - 编译器会为你强制执行。让编译器帮你避免错误几乎总是一个好主意。

至于膨胀 - 我需要看到证据。 std::unique_ptr 是一个指针类型的薄包装器,一个好的优化编译器应该可以毫不费力地为它生成好的代码。的确,有构造函数、析构函数等与 std::unique_ptr 相关联,但合理的编译器会内联这些调用,它们基本上只是做与您最初描述的相同的事情。

您基本上是依靠 class 来管理指针的生命周期,但忽略了指针在函数之间传递、从函数返回并且通常无处不在。如果您的示例中的指针需要比 class 更长寿怎么办?如果需要在class销毁前删除怎么办?

考虑这个函数:

Bar * doStuff(int param) {
    return new Bar(param);
}

您现在有一个动态分配的对象,如果您忘记删除它可能会泄漏。也许您没有阅读文档,或者可能缺少文档。无论如何,这个函数会给你带来不必要的负担来销毁 Bar.

的返回实例

现在考虑:

std::unique_ptr<Bar> doStuffBetter(int param) {
    return new Bar(param);
}

返回的 unique_ptr 管理它包装的指针的生命周期。函数 returns a unique_ptr 消除了关于所有权和生命周期的任何混淆。一旦返回的 unique_ptr 超出范围并调用其析构函数,Bar 的实例将自动删除。

unique_ptr 只是标准库提供的几种方法之一,这些方法使使用指针的过程变得不那么混乱,并表达了所有权。它既轻巧又像普通指针一样工作,除了复制。

使用 std::unique_ptr (RAII) 而不是原始指针更容易成为 exception-safe。

考虑一个 class 有两个成员变量在其构造函数中获取内存。

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
        car = new Car;    // <- What happens when this throws exception?
    }

    ~Foo()
    {
        if(bar)
            delete bar;
        if(car)
            delete car;
    }
private:
    Bar* bar;
    Car* car;

};

如果Foo的构造函数抛出异常,则Foo没有构造成功,因此不会调用其析构函数。 当new Car抛出异常时,bar并没有被删除,所以会出现内存泄漏。

现在考虑使用 std::unique_ptr 的代码。

class Foo
{
public:
    Foo() : bar(std::make_unique<Bar>()), car(std::make_unique<Car>()) {}

private:
    std::unique_ptr<Bar> bar;
    std::unique_ptr<Car> car;

};

如果Foo的构造函数抛出异常,则Foo没有构造成功,因此不会调用其析构函数。 但是,调用成功创建的成员实例的析构函数。 即使std::make_unique<Car>()抛出异常,也会调用bar的析构函数,所以不会有内存泄漏。