GotW #101“解决方案”真的解决了啥问题吗?
Posted
技术标签:
【中文标题】GotW #101“解决方案”真的解决了啥问题吗?【英文标题】:Does the GotW #101 "solution" actually solve anything?GotW #101“解决方案”真的解决了什么问题吗? 【发布时间】:2012-01-25 14:21:50 【问题描述】:首先阅读 Herb 的 Sutters GotW 关于 C++11 中的 pimpl 的帖子:
GotW #100: Compilation Firewalls (Difficulty: 6/10)
GotW #101: Compilation Firewalls, Part 2 (Difficulty: 8/10)
我在理解 GotW #101 中提出的解决方案时遇到了一些问题。据我所知,在 GotW #100 中辛苦解决的所有问题都卷土重来:
pimpl
成员是离线模板,在使用时定义是不可见的(在class widget
的类定义和隐式生成的widget
的特殊成员函数中) .也没有任何明确的实例化。这将在链接期间导致未解决的外部错误。
widget::impl
在 pimpl<widget::impl>::~pimpl()
被 instantiated 定义的地方仍然不完整(我认为它实际上根本没有被实例化,只是被引用)。所以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,如果你路过,请告诉我是否可以在此处剪切+粘贴解决方案代码以供参考。
【问题讨论】:
Paging
Dr. @HerbSutter
@Ben Voigt:我确实使用 gotw 搜索并标记了其他几个问题。你找到了我错过的一些吗?
@Ben Voigt:那么一定是 Stack Exchange 中的传播延迟。在为这个问题创建标签后,我立即将标签添加到其他几个问题中。从那时起,我又发现了一个问题,并对其进行了标记。
我刚刚阅读了文章,我在理解它如何更好方面遇到了同样的困难——因为显然不是,或者代码不完整。我通过谷歌“gotw #101 不起作用”到达那里 :)
【参考方案1】:
你是对的;该示例似乎缺少显式模板实例化。当我尝试在 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,您需要的样板很简单。
【讨论】:
widget::~widget()
需要调用所有子对象的析构函数。 pimpl<widget::impl>::~pimpl()
在哪里实例化,以便widget::~widget()
可以调用?
@BenVoigt:它在使用时被实例化,即widget::~widget()
在.cpp
文件中的定义。
@MikeSeymour:GotW #100 版本在 .cpp 文件中定义了 widget::~widget()
。 GotW #101 没有,它说它“消除了一些样板”,并特别提到了析构函数。
@BenVoigt,你是对的。我尝试编译它以确保,pimpl
的隐式实例化不包括在实现widget::impl
时实例化其构造函数或析构函数。【参考方案2】:
我认为混淆是这样的:pimpl 包装器可能是一个模板,而小部件类不是:
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
如您所见,没有人会看到小部件类的“模板化”构造函数。它只有一个定义,不需要“显式实例化”。
相反,~pimpl<>
析构函数只是曾经从~widget()
析构函数的单点定义实例化。根据定义,此时impl
类已完成。
没有链接错误,也没有 ODR/UB 违规。
奖金/额外福利
正如 Herb 本人在他的帖子 (Why is this an improvement over the hand-rolled Pimpl Idiom?1) 中恰当地解释的那样,使用这个 pimpl 包装器还有很多优势,这源于重用实现的核心:
使用模板防止不必要地陷入陷阱 您可以使用 C++0x/C++11 获得可变参数、完美的转发构造函数,无需梦想具有右值引用可变参数列表的模板化构造函数模板将参数包转发到包装类的构造函数 完美等等等等。简而言之:干燥且方便。
1 引用(强调我的):
首先,代码更简单,因为它消除了一些样板:在手卷版本中,您还必须声明构造函数并将其主体写入实现文件和显式分配 impl 对象。您还必须记住声明析构函数并将其主体写入实现文件,因为在 #100 中解释了晦涩的语言原因。 其次,代码更健壮:在手卷版本中,如果您忘记编写外联析构函数 Pimpl 'd 类将单独编译并且似乎处于可签入状态,但当调用者尝试销毁对象并遇到有用的“无法生成析构函数,因为 impl 是,呃,你知道,不完整”的错误让调用代码的作者在走到你的办公室时摸不着头脑,因为他检查了一些损坏的东西。
【讨论】:
添加了一些关于 pimpl 包装器解决了什么的更明显的论点,除了 OP 问题的重点。 但是 Herb 的“解决方案”没有非默认的widget::~widget
或离线定义。他说“在手卷版本中,您必须记住声明析构函数并将其主体写入实现文件”,您现在说您仍然需要为包装器执行此操作。这与稳健性项目符号中的主张相矛盾。
我刚刚从他的代码中复制了那个 out-of-line 析构函数?现在,如果你认为默认它可能会导致同样的问题,我不得不参考标准,但我强烈建议编译器应该发出 defaulted 析构函数 在声明~widget() = default;
的点,即impl
是一个完整类型的时间。
sehe,您是从 GotW #101 中标有“解决方案”的子标题上方还是下方复制?
总结:Herb 说包装“消除了一些样板”。如果这些被消除,代码似乎不起作用。以上是关于GotW #101“解决方案”真的解决了啥问题吗?的主要内容,如果未能解决你的问题,请参考以下文章