是否有可能在c ++中编写敏捷的Pimpl?

时间:2014-07-08 15:03:38

标签: c++ c++11 pimpl-idiom

我一直在玩Pimpl成语并从中获得各种好处。我唯一对此过于热衷的是我在定义函数时得到的感觉。

  • 一旦进入标题(P def)
  • 一旦位于.cpp(Impl def)
  • 的顶部
  • 一旦进入.cpp(Impl Impl)
  • 的中间
  • 一旦位于.cpp(P Impl)的下端

我非常喜欢减少代码差异和冗余,当我在当前项目中即使是相对复杂的Impls中添加或更改函数时,我觉得我的代码还不够好。

我的问题是,有哪些有效的方法可以暗示或模板化我的类,如果我要定义一个新函数,我只需编写一个明确的定义和实现,并拥有其余的仍然在空间上接近代码中的explicits;如果我要更改功能,必要的更改将尽可能少?

4 个答案:

答案 0 :(得分:8)

你可能会考虑以下几点:

用于最小化重复声明的Interface类。客户端将在其代码中使用PublicImplementation类。

<强> Pimpl.h

#ifndef PIMPL_H_
#define PIMPL_H_

#include <memory> // std::unique_ptr

class Interface
{
public:
    virtual ~Interface() {}

    virtual void func_a() = 0;
    virtual void func_b() = 0;
};

class PublicImplementation
{
    // smart pointer provides exception safety
    std::unique_ptr<Interface> impl;

public:
    PublicImplementation();

    // pass-through invoker
    Interface* operator->() { return impl.get(); }
};

#endif // PIMPL_H_

<强> Pimpl.cpp

#include "Pimpl.h"
#include <iostream>

class PrivateImplementation
: public Interface
{
public:

    void func_a() override { std::cout << "a" << '\n'; }
    void func_b() override { std::cout << "b" << '\n'; }
};

PublicImplementation::PublicImplementation()
: impl(new PrivateImplementation)
{
}

最后这就是客户端代码的作用:

<强> Main.cpp的

#include "Pimpl.h"

int main()
{
    PublicImplementation pi; // not a pointer

    pi->func_a(); // pointer semantics
    pi->func_b();
}

答案 1 :(得分:4)

让我们假设你的标题开头是这样的:

class X
{
  public:
    ...la de dah...
  private:
    struct Impl;
    Impl* p_impl_;
};

然后当您添加功能时,您可以选择:

  1. 您是否有X成员函数定义实现逻辑,在整个地方引用p_impl_->个事物,或者

  2. return p_impl->same_fn(all_the_args);并将逻辑保留在Impl类中?

  3. 如果你选择1.那么你最终会在标题中添加一个函数声明,并在匹配的实现文件中找到一个(比通常稍微麻烦)的定义。

    如果你选择2.那么你最终会在头文件中有一个函数声明,在匹配的实现文件中有一个包装/转发定义,并且至少在Impl结构中有一个定义(我倾向于定义Impl类定义之外的函数 - 它是一个实现细节,并且接口不是公共的。)

    通常没有理由改进这种情况(即,构建过程中的宏hackery和额外的代码生成脚本有时可能是有保证的,但非常少)。


    它可能并不重要,尽管第二种方法的变体可能是有意义的,首先要实现一个使用pimpl习语的类(带有正确的标题)然后你可以使用pimpl管理对象包装它并将函数转发给它,这样你就可以自由地在某个地方某些地方决定它想要使用它不使用pimpl包装器的功能,可能是为了提高性能/减少内存使用,代价是重新编译依赖。您也可以使用模板的特定实例化而不暴露模板的代码。

    为了说明这个选项(如评论中所要求的那样),让我们从它自己的文件中的一个愚蠢的非pimpl class X开始,然后创建一个Pimpl::X包装器(使用命名空间和相同的类名是完全可选的,但有助于翻转客户端代码以使用它们和提醒 - 这并不意味着简洁,这里的重点是让非pImpl版本也可用:)

    // x.h
    class X
    {
      public:
        int get() const { return n_; }   // inline
        void operator=(int);  // out-of-line definition
      private:
        int n_;
    };
    
    // x.c++
    #include <x.h>
    void X::operator=(int n) { n_ = n * 2; }
    
    // x_pimpl.h
    namespace Pimpl
    {
        class X
        {
          public:
            X();
            X(const X&);
            ~X();
            X& operator=(const X&);
            int get() const;
            void operator=(int);
          private:
            struct Impl;
            Impl* p_impl_;
        };
    }
    
    x_pimpl.c++
    #include <x.h>
    namespace Pimpl
    {
        struct X::Impl
        {
            ::X x_; 
        };
    
        // the usual handling...
        X() : p_impl_(new Impl) { }
        X(const X& rhs) : p_impl(new Impl) { p_impl_->x_ = rhs.p_impl_->x_; }
        ~X() { delete p_impl_; }
        X& operator=(const X& rhs) { p_impl_->x_ = rhs.p_impl_->x_; return *this; }
    
        // the wrapping...
        int X::get() const { return p_impl_->x_.get(); }
        void X::operator=(int n) { p_impl_->x_ = n; }
    }
    

    如果您选择2上面的上述变体,这使得“实现”本身就是一个可用的实体,那么是 - 您可能最终得到2个声明和2个与单个函数相关的定义,但是定义将是一个简单的包装/转发函数,如果函数非常短且数量众多但参数很多,那么这个函数只会显着重复且繁琐。

答案 2 :(得分:3)

没有要求将IMPL对象视为相同的规则&amp;标准作为.h文件中的对象声明。通过允许成员变量公开(通过struct声明),您不需要实现不必要的包装器层。这通常是安全的,因为只有.cpp文件才能访问IMPL。

考虑下面的代码,它可以实现PIMPL习惯用法的好处,而不会产生不必要的代码重复:

// x.h
class X {
public:
    X();
    ~X();

    X(const X&) = delete;
    X& operator =(const X&) = delete;

    void set(int val);
    int get() const;

private:
    struct IMPL;
    IMPL* impl;
};

// x.cpp
#include "x.h"

struct X::IMPL {
    int val;
};


X::X() : impl(new IMPL) {}

X::~X() { delete impl; }

void X::set(int val)
{
    impl->val = val;
}

int X::get() const
{
    return impl->val;
}

// main.cpp
#include <iostream>
#include "x.h"

int main (int, char *[])
{
    X x;
    x.set(10);
    std::cout << x.get() << std::endl;
    return 0;
}

答案 3 :(得分:1)

我只是开始通过sumarizing来确保我理解:你喜欢使用pimpl的好处,但是在添加或修改函数时不喜欢样板代码的数量?

简而言之,没有模板魔法可以用来消除这个样板,但是这里也有一些事情需要考虑:

  • 您只编写一次代码但多次阅读,您可以使用各种复制粘贴功能。最初创建函数不是您在此课程上花费的大部分时间。编译和维护是您花费时间的地方。
  • 确保公共类API尽可能简单。公共API中的函数越少,您编写的样板就越少。您可以在impl中创建任意数量的函数,只需在那里修改它们。
  • 如果您发现自己多次更改公共类API,您可能希望稍微调整您的设计过程。花费十多分钟预先查看/写下用例,您可以将API更改减少90%。