【问题标题】:pimpl for a templated class用于模板类的 pimpl
【发布时间】:2011-10-22 08:18:21
【问题描述】:

我想使用 pimpl 习惯用法来避免我的库的用户需要我们的外部依赖项(如 boost 等),但是当我的类被模板化时,这似乎是不可能的,因为方法必须在标题中。有什么我可以代替的吗?

【问题讨论】:

    标签: c++ idioms pimpl-idiom


    【解决方案1】:

    如果类是模板化的,您的用户基本上需要编译它(这在最广泛使用的 C++ 实现中确实如此),因此他们需要您的外部依赖项。

    最简单的解决方案是将类的大部分实现放在非模板基类(或某个类的封装成员对象)中。解决那里的模块隐藏问题。

    然后编写模板派生(或封闭)类为其添加类型安全性。

    例如,假设您有一个模板,它提供了惊人的首次访问分配能力(省略了必要的复制构造函数、赋值、析构函数):

    template <class T>
    class MyContainer
    {
        T *instance_;
    
    public:
        MyContainer() : instance_(0) {}
    
        T &access()
        {
            if (instance_ == 0)
                instance_ = new T();
    
            return *instance_;
        }
    };
    

    如果你想把“逻辑”分离成一个非模板基类,你必须以非模板的方式参数化行为,也就是说,使用虚函数:

    class MyBase
    {
        void *instance_;
    
        virtual void *allocate() = 0;
    
    public:
        MyBase() : instance_(0) {}
    
        void *access()
        {
            if (instance_ == 0)
                instance_ = allocate();
    
            return instance_;
        }
    };
    

    然后你可以在外层添加类型感知:

    template <class T>
    class MyContainer : MyBase
    {
        virtual void *allocate()
            { return new T(); }
    
    public:
        T &access()
            { return *(reinterpret_cast<T *>(MyBase::access())); }
    };
    

    即您使用虚函数来允许模板“填充”依赖于类型的操作。显然,只有当您有一些值得努力隐藏的业务逻辑时,这种模式才真正有意义。

    【讨论】:

    • 我认为如果您不希望预处理器定义 (#define)、常量等可见,这种方法也很有用。作为开发人员,我不想看到我正在使用的类/库的实现细节,尤其是在自动完成列表中。即使您的业务逻辑不值得隐藏,您也可能希望隐藏它们。
    • 如果你使用CRTP,你可以避免使用虚函数。
    • 如果您想避免使用模板(CRTP 中的 T),则不需要,因为您需要单独编译的模块。
    【解决方案2】:

    您可以在源文件中显式地实例化模板,但这只有在您知道模板类型的情况下才有可能。否则,不要将 pimpl idiom 用于模板。

    类似这样的:

    header.hpp:

    #ifndef HEADER_HPP
    #define HEADER_HPP
    
    template< typename T >
    class A
    {
      // constructor+methods + pimpl
    };
    
    #endif
    

    source.cpp:

    #include "header.hpp"
    
    // implementation
    
    // explicitly instantiate for types that will be used
    template class A< int >;
    template class A< float >;
    // etc...
    

    【讨论】:

    • -1 不要将auto_ptr 用于 PIMPL(使用不完整类型实例化auto_ptr未定义的行为)。如果为外部类同时定义构造函数和析构函数,它确实有效。但在这种情况下,您不需要智能指针。
    • 是的,两者都可以(虽然我不确定在这种情况下如何使用unique_ptr 的细节;我只会使用shared_ptr 并接受开销作为不需要的成本人们阅读标准中的小字)。干杯,
    • @Alf 修正了这个例子,但是哪一段确切地告诉用不完整的类型实例化auto_ptr 是无效的?
    • unique_ptr 也需要用户定义的析构函数,就像 auto_ptr 所做的那样。老实说,我不认为这是一个问题......它不需要任何事情,只是不是编译器生成的,并在Impl完成后显示。
    • @DanielEarwicker 在A 的复制 ctor 成员初始化列表中,您可以写为pimpl(new *right.pimpl.get())。您描述的用法不是复制pimpl,而是分享它。这是一个非常大的区别,通常是不受欢迎和损坏的——但是这样做可能是一种设计选择。顺便说一句,为了解决你最后一行自我旅行的问题 - 除非你是 RIGHT,否则不要这样做。
    【解决方案3】:

    一般有两种解决方案:

    • 虽然接口依赖于某些类型 T,但它遵循更弱类型的实现(例如,直接使用 void* 指针或槽类型擦除),或者

    • 您只支持特定且数量有限的类型。

    第二种解决方案适用于例如char/wchar_t 依赖的东西。

    第一个解决方案在 C++ 模板的早期很常见,因为当时编译器不善于识别生成的机器代码中的共性,并且会引入所谓的“代码膨胀”。今天,令任何尝试过的新手感到惊讶的是,模板化解决方案通常比依赖运行时多态性的解决方案具有更小的机器代码占用空间。当然,YMMV。

    干杯,

    【讨论】:

    • 有人尝试编辑建议删除“直接或通过类型擦除”详细说明和“特定和...”限定。详尽是意义所必需的,而限定是正确性所必需的。当前接受的解决方案答案是“直接”的一个例子。目前还没有“类型擦除”的例子,唯一提到的是这个答案;如果有人成功删除了那将是一种耻辱。
    猜你喜欢
    • 1970-01-01
    • 2014-01-16
    • 1970-01-01
    • 1970-01-01
    • 2022-11-01
    • 2012-01-13
    • 1970-01-01
    • 1970-01-01
    • 2017-09-09
    相关资源
    最近更新 更多