GotW#101“解决方案”实际上解决了什么问题吗?

时间:2011-12-21 19:50:08

标签: c++ c++11 pimpl-idiom incomplete-type gotw

首先阅读Herb's Sutters GotW关于C ++ 11中pimpl的帖子:

我在理解GotW#101中提出的解决方案时遇到了一些麻烦。据我所知,在GotW#100中辛苦解决的所有问题都复仇了:

  • pimpl成员是外联模板,定义在使用时不可见(在class widget的类定义中并隐式生成的特殊成员函数widget)。也没有任何明确的实例化。这将导致链接期间未解决的外部错误。

  • widget::implpimpl<widget::impl>::~pimpl() 实例化定义的位置仍然不完整(我认为它实际上根本没有被实例化,只是被引用)。所以std::unique_ptr<widget::impl>::~unique_ptr()在指向不完整类型的指针上调用delete,如果widget::impl具有非平凡的析构函数,则会产生未定义的行为。

请解释是什么迫使编译器在widget::impl完成的上下文中生成特殊成员。因为我看不出它是如何工作的。


如果GotW#101仍然需要在实现文件中明确定义widget::~widget()widget::impl已完成,那么请解释“更健壮”的评论(@sehe在他的回答中引用)。

我正在研究GotW#101的核心主张,即封装“消除了一些样板”,在我看来(基于段落的其余部分)是widget::~widget()声明和定义。所以请不要依赖你的答案,在GotW#101中,那已经消失了!


Herb,如果你停下来,请告诉我是否可以在此处剪切+粘贴解决方案代码以供参考。

2 个答案:

答案 0 :(得分:9)

你是对的;该示例似乎缺少显式模板实例化。当我尝试在MSVC 2010 SP1上使用widget::impl的构造函数和析构函数运行示例时,我收到pimpl<widget::impl>::pimpl()pimpl<widget::impl>::~pimpl()的链接器错误。当我添加template class pimpl<widget::impl>;时,它链接正常。

换句话说,GotW#101从GotW#100中删除了所有样板,但是您需要添加pimpl<...>模板的显式实例化以及pimpl impl的实现。至少#101,您需要的锅炉板很简单。

答案 1 :(得分:6)

我认为混淆是这样的:pimpl包装器可能是一个模板,widget类不是:

demo.h

#include "pimpl_h.h"

// in header file
class widget {
public:
    widget();
    ~widget();
private:
    class impl;
    pimpl<impl> pimpl_;
};

demo.cpp

#include "demo.h"
#include "pimpl_impl.h"

// in implementation file
class widget::impl {
    // :::
};

widget::widget() : pimpl_() { }
widget::~widget() { } // or =default

正如您所看到的,没有人会看到widget类的“模板化”构造函数。它只有一个定义,不需要“显式实例化”。

相反,~pimpl<>析构函数只是从~widget()析构函数的单点定义实例化 。此时,根据定义,impl类已完成。

没有链接错误,也没有ODR / UB违规。

奖金/额外福利

正如Herb自己在帖子( Why is this an improvement over the hand-rolled Pimpl Idiom?1 )中恰当地解释的那样,使用这个pimpl包装器还有很多优点,这源于重用实现内容:

  • 使用模板防止不必要地陷入陷阱
  • 你得到了具有C ++ 0x / C ++ 11的可变,完美的转发构造函数,无需梦想带有rvalue-reffed可变参数列表的模板化构造函数模板将参数包转发给包装类的构造函数完美等等。

简而言之:干燥和方便。


<子> 1 引用(强调我的):

  
      
  • 首先,代码更简单,因为它消除了一些样板:在手动滚动版本中,您还必须声明构造函数并将其主体写入实现文件中,显式分配impl对象。您还必须必须记住来声明析构函数并在实现文件中编写其正文,因为模糊的语言原因在#100中解释。
  •   
  • 其次,代码更健壮:在手动滚动版本中,如果您忘记来编写外联析构函数,那么Pimpl 'class将单独编译并且看起来处于可检入状态,但是当调用者试图销毁对象并遇到有用的“无法生成析构函数时,将无法编译,因为impl是,呃,你知道,不完整的“错误使得调用代码的作者在他走到你的办公室时为了检查一些破碎的东西而把你弄得头疼。”
  •