【问题标题】:Why should I use an std::unique_ptr when I could just destroy the object in my destructor?当我可以在我的析构函数中销毁对象时,为什么要使用 std::unique_ptr ?
【发布时间】:2016-09-19 03:19:01
【问题描述】:

假设我有这门课:

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

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

};

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

【问题讨论】:

  • std::unique_ptr 肿到什么程度了?它是一个非常薄的原始指针包装器。
  • 因为你会忘记。也许不是现在,而是以后。你会改变一些东西而忘记改变其他东西。现在你有内存泄漏。或者你会双倍释放它。此外,它不是“臃肿”
  • 为了证明这一点,看看这个构造函数和 getter 函数花了我多少钱here。请注意,编译器只是加载值并打印它,完全优化了对我的类的任何引用。这忽略了这样一个事实,即复制 8 个字节的构造函数调用在几乎所有情况下都完全可以忽略不计。
  • 您的朋友正在过早地优化,因此他们的代码将变得一团糟。编写干净的代码,使用语言来帮助您正确编写代码。
  • 在我的测试中,unique pointerraw pointer 一样快,并且在检查生成的汇编语言时,编译器生成了相同的代码取消引用一个唯一指针和一个原始指针。优化器在取消引用时完全删除了std::unique_ptr

标签: c++ pointers destructor unique-ptr


【解决方案1】:

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

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;
};

哇,这要短得多!并且它会自动确保类不能被复制,并且它使默认的移动构造函数和移动赋值运算符正常工作。

所以std::unique_ptr 的一个巨大优势是它可以自动处理资源管理,是的,但另一个优势是它可以很好地处理复制和移动语义,并且不会以意想不到的方式工作。这是使用它的主要原因之一。你可以说出你的意思——“我是唯一应该知道这件事的人,你不能分享它”——编译器会为你强制执行。使用编译器来帮助您避免错误几乎总是一个好主意。

至于臃肿——我需要看看这方面的证据。 std::unique_ptr 是指针类型的瘦包装器,一个好的优化编译器应该可以毫无问题地为它生成好的代码。确实,有与 std::unique_ptr 相关联的构造函数、析构函数等,但合理的编译器会内联这些调用,它们的作用基本上与您最初描述的相同。

【讨论】:

  • 我笑一下可以吗..只是一点点? “但我的例子太简单了”..但海报仍然搞砸了。
  • @Snorflake 我不确定你朋友的意思。我认为上述答案与懒惰无关。 (另外,真的很迂腐,但希望以一种有启发性的方式:术语“STL”通常指的是 std::vectorstd::map 之类的东西,它们最初是一个名为 STL 的库的一部分,但后来被合并了(部分) 到语言中。我不会真正将std::unique_ptr 称为 STL 的一部分。)
  • @Snorflake 我假设您的朋友也使用原始的char*s 并在每次需要时编写自己的动态数组实现。您使用标准库是因为它们解决了问题。为什么每次编写新程序时都要自己解决?
  • 你的朋友对日常问题的简单解决方案有一种奇怪的反感。
  • @Snorflake: "因为我懒得避免使用 STL" 避免使用标准库的任何特定元素是有真正的理由的。但是,声称使用良好、经过良好测试且非常有用的代码是懒惰,这完全是荒谬的。这个人似乎不讲道理,所以我建议你不要听他们的建议。
【解决方案2】:

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

考虑这个函数:

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

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

现在考虑:

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

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

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

【讨论】:

    【解决方案3】:

    使用std::unique_ptr (RAII) 代替原始指针更容易实现异常安全。

    假设一个类有两个在其构造函数中获取内存的成员变量。

    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&lt;Car&gt;() 抛出异常,调用bar 的析构函数,也不会发生内存泄漏。

    【讨论】:

      猜你喜欢
      • 2021-07-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-16
      • 1970-01-01
      • 1970-01-01
      • 2016-08-04
      • 1970-01-01
      相关资源
      最近更新 更多