pImpl技法中应该使用std::auto_ptr还是boost::shared_ptr?

16

在使用pImpl惯用法时,是否优先使用boost::shared_ptr而不是std::auto_ptr? 我记得曾经读到过boost版本更加友好于异常处理?

class Foo
{
public:
    Foo();
private:
    struct impl;
    std::auto_ptr<impl> impl_;
};

class Foo
{
public:
    Foo();
private:
    struct impl;
    boost::shared_ptr<impl> impl_;
};

[编辑] 是否总是安全使用std::auto_ptr<>,或者是否存在需要另一种替代的boost智能指针的情况?


2
有一件事真的让我很困扰:你愿意使用pimpl惯用法,这是一个编译器防火墙。因此,你试图限制编译时依赖关系。然而,你却在愉快地注入智能指针实现(scoped_ptrshared_ptrauto_ptr或其他),而你的“shell”类仅仅是将调用转发到实现类并在其析构函数中删除指针。不知怎么的,如果shell类做了更多的事情,那么设计可能会出问题。 - Gregory Pakosz
我认为这个需要更新到新的C++版本。Herb Sutter最近在他的博客中提到了这个问题。 - Björn Pollex
类似问题:https://dev59.com/FG035IYBdhLWcg3wMM4G - Rolf Kristensen
9个回答

38

您不应该真正使用std::auto_ptr来做这件事。在您声明std::auto_ptr时,析构函数可能不可见,因此可能无法正确调用。这是假设您正在前向声明pImpl类,并在另一个文件的构造函数中创建实例。

如果您使用boost::scoped_ptr(这里不需要shared_ptr,您不会与任何其他对象共享pimpl,并且这是通过scoped_ptr为noncopyable而强制执行的),则只需要在调用scoped_ptr构造函数时使pimpl析构函数可见。

例如:

// in MyClass.h

class Pimpl;

class MyClass 
{ 
private:
    std::auto_ptr<Pimpl> pimpl;

public: 
    MyClass();
};

// Body of these functions in MyClass.cpp

在这里编译器将会生成MyClass的析构函数。该析构函数必须调用auto_ptr的析构函数。在实例化auto_ptr析构函数时,Pimpl是一个未完成的类型。所以当auto_ptr析构函数删除Pimpl对象时,它将不知道如何调用Pimpl的析构函数。

boost::scoped_ptr(和shared_ptr)没有这个问题,因为当你调用scoped_ptr的构造函数(或reset方法)时,它也会创建一个函数指针等效物,它将用来代替调用delete。关键点在于,在Pimpl不是未完成类型时,它会实例化释放函数。顺便说一句,shared_ptr允许您指定自定义回收函数,因此您可以将其用于GDI句柄之类的东西 - 但这对您在此处的需求来说过于繁琐。

如果您确实想使用std::auto_ptr,则需要特别小心,确保您在Pimpl完全定义时在MyClass.cpp中定义MyClass析构函数。

// in MyClass.h

class Pimpl;

class MyClass 
{ 
private:
    std::auto_ptr<Pimpl> pimpl;

public: 
    MyClass();
    ~MyClass();
};

并且

// in MyClass.cpp

#include "Pimpl.h"

MyClass::MyClass() : pimpl(new Pimpl(blah))
{
}

MyClass::~MyClass() 
{
    // this needs to be here, even when empty
}

编译器将生成代码,在空析构函数中有效地销毁MyClass成员。因此,在实例化auto_ptr析构函数时,Pimpl不再是不完整的,并且编译器现在知道如何调用析构函数。 个人而言,我认为确保一切正确并不值得麻烦。还有一个风险,即后来有人通过删除看似多余的析构函数来整理代码。因此,对于这种情况,使用boost::scoped_ptr更加安全可靠。

1
但是,当您使用scoped_ptr时,由于它不可复制,因此无法调用pimpl(new Pimpl(blah))。那么shared_ptr不是更好吗? - Frank
5
你为什么要将Pimpl类声明在MyClass之外?我问这个问题是因为我已经习惯使用一个私有结构体来作为pimpl对象。 - jamuraa
3
这个答案还正确吗?至少在 Boost 1.41 中,scoped_ptr 看起来需要在类完成定义时定义析构函数和构造函数,并且它会直接调用 delete。 - Amnon
GCC在使用auto_ptr的pimpl时会报错,提示缺少析构函数,因此析构函数的实现不会被遗忘。我认为这将问题降至无关紧要,并使auto_ptr优于boost解决方案。 - Basilevs
4
此答案包含错误信息。 boost::scoped_ptr 不像 shared_ptr 那样在构造时使用类型擦除来捕获删除器,std::unique_ptr 也是如此。它们两个与 std::auto_ptr 相同,存在关于不完全类型的问题。请参见 scoped_ptrGOTW 100 - Andrew Durward

12

我倾向于使用 auto_ptr。请确保将您的类设置为不可复制(声明私有复制构造函数和运算符=,或者继承 boost::noncopyable)。如果使用 auto_ptr,一个需要注意的问题是,即使空体,也需要定义非内联析构函数。(这是因为如果让编译器生成默认析构函数,在生成调用 delete impl_ 时,impl 将是不完整的类型,从而导致未定义的行为)。

auto_ptr 和 boost 指针之间几乎没有什么区别。如果标准库提供的替代品可以胜任,出于风格上的考虑,我倾向于不使用 boost。


3
如果您没有用户定义的析构函数,为什么会导致未定义行为?能否提供更多细节(也许是标准章节)? - Johannes Schaub - litb
标准中没有明确说明,但这是编译器在看到没有声明析构函数时必须实例化外部类的内联默认析构函数的结果。由于存在auto_ptr<impl>成员变量,因此默认析构函数必须调用auto_ptr<impl>的析构函数。 - fizzer
相反地,如果声明了一个析构函数,编译器将不会生成默认的析构函数。然后,在您的 CPP 文件中,您在顶部定义结构体 impl,然后是(可能为空的)析构函数主体。同样,auto_ptr<impl>被实例化,但这一次 impl 是完整的,并且 delete 被定义。 - fizzer
我认为这是标准的意外后果。如果你在comp.lang.c++.moderated中搜索“auto_ptr incomplete type”,你会发现比我聪明的人们进行了更好的分析。另请参阅boost smart_ptr页面,其中讨论了该问题以及为什么它们的某些指针不会受到影响。 - fizzer
兼容性问题使得这个选择同样困难。 - Basilevs
显示剩余3条评论

4
boost替代std::auto_ptr的是boost::scoped_ptr。与auto_ptr的主要区别在于,boost::scoped_ptr是不可复制的。
有关更多详细信息,请参见此页面

这不对。auto_ptr 应该是可移动的,而 scoped_ptr 不是。你提供的文档也提到了这一点:“使用 scoped_ptr 而不是 auto_ptr 的主要原因是让代码读者知道你打算仅将“资源获取即初始化”应用于当前作用域,并且没有意图转移所有权。” - Sebastian Mach

4

boost::shared_ptr是专门为pimpl惯用法量身定制的。其中一个主要优点是允许不为持有pimpl的类定义析构函数。共享所有权策略既可能是优势也可能是劣势。但在后一种情况下,您可以适当地定义拷贝构造函数。


即使它是为pimpl习惯定制的,它也带着自己的本质;你共享impl,这意味着默认情况下两个对象的副本实际上是同一个对象。这种行为令人惊讶,你最不想做的就是让你的类的用户感到惊讶。 - daramarak
你可能想要两个副本拥有相同的对象。 - CashCow

1
如果您想要一个可复制的类,请使用scoped_ptr,它禁止复制,从而使您的类默认情况下难以被错误使用(与使用shared_ptr相比,编译器不会自动发出复制设施;并且在shared_ptr的情况下,如果您不知道自己在做什么[即使对于专家来说,这种情况也经常发生],当突然有一个东西的副本也修改了那个东西时,会出现奇怪的行为),然后定义一个复制构造函数和复制赋值函数:
class CopyableFoo {
public:
    ...
    CopyableFoo (const CopyableFoo&);
    CopyableFoo& operator= (const CopyableFoo&);
private:
    scoped_ptr<Impl> impl_;
};

...
CopyableFoo (const CopyableFoo& rhs)
    : impl_(new Impl (*rhs.impl_))
{}

你应该从boost::noncopyable派生你的类,使其不可复制。 - fmuecke
@fmuecke:但是我的CopyableFoo旨在可复制。使用scoped_ptr<>可以确保您不会忘记编写复制分配和构造函数。因为scoped_ptr<>本身不可复制,所以编译器不会尝试自动发出它们。如果您使用shared_ptr<>,编译器将发出可能不符合您意图的复制操作。请记住,我的类旨在可复制,而不是不可复制。当然,您是正确的,应该使用boost::noncopyable来实现这一点。 - Sebastian Mach

1

如果你非常严谨,使用auto_ptr成员并不能绝对保证在使用时不需要auto_ptr的模板参数的完整定义。尽管如此,我从未见过这种情况失败。

一种变化是使用const auto_ptr。只要您可以在初始化列表中使用new表达式构造您的“pimpl”,就可以保证编译器无法生成默认的复制构造函数和赋值方法。仍然需要提供封闭类的非内联析构函数。

其他条件相同的情况下,我更喜欢使用仅使用标准库的实现,因为它使事情更具可移植性。


shared_ptr和scoped_ptr在TR1标准库中。 :-) - C. K. Young
正确,但TR1不是1998或2003标准的一部分,许多C++环境可以合理地实现这些标准,但不提供TR1库。您的代码依赖性越少,可移植性就越强。 - CB Bailey
shared_ptr在TR1中。scoped_ptr不在其中。 - Rimo

0

对于pImpl,shared_ptr比auto_ptr更可取,因为当你复制它时,外部类可能会突然丢失其指针。

使用shared_ptr时,你可以使用前向声明类型,这样就能正常工作。auto_ptr不允许前向声明类型。scoped_ptr也不允许,如果你的外部类无论如何都不可复制,并且只有一个指针,那么它最好是一个常规指针。

在pImpl中使用侵入式引用计数有很多好处,让外部类在其实现中调用其复制和赋值语义。假设这是一个真实的供应商(提供类)模型,最好是供应商不强迫用户使用shared_ptr,也不强迫用户使用相同版本的shared_ptr(boost或std)。


0

我非常喜欢 Vladimir Batov 改进版的 impl_ptr。它使得创建一个 pImpl 变得非常容易,而不需要显式地复制构造函数和赋值运算符。

我修改了原始代码,现在它类似于 shared_ptr,因此可以在尾声代码中使用,并保持高速运行。


-8

不要试图在C++中自己给自己找麻烦,因为你有很多机会 :) 既然你完全知道对象何时应该进入和退出生命周期(在构造函数和析构函数中),所以没有真正的必要使用auto指针。

保持简单。


7
强烈不同意。这种方法不能防止在构造函数本身中引起的异常,这些异常可能会在创建您的实现对象后发生。当发生这种情况时,析构函数不会被调用,您的实现对象将被泄漏。 - C. K. Young
如果您使用pimpl习惯用法,那么impl指针很可能是类中唯一的指针。在这种情况下使用非托管指针是没有问题的,只要析构函数删除指针即可。由于只有一个成员,因此构造函数中不可能存在资源泄漏,只有new可能抛出异常并且不会分配内存。 - daramarak
问题在于现在你必须在析构函数中删除一个资源,因此你还必须实现复制和赋值语义,而且可能最终要进行引用计数,可能会采用侵入式方法。 - CashCow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接