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

AAo*_*ris 6 c++ pimpl-idiom c++11

我一直在玩Pimpl成语并从中获得各种好处.我唯一不太关注的是我在定义函数时得到的感觉.

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

我真的很喜欢减少代码差异和冗余,当我必须在当前项目中相对复杂的Impls中添加或更改函数时,我觉得我的代码不够好.

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

Gal*_*lik 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_
Run Code Online (Sandbox Code Playgroud)

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)
{
}
Run Code Online (Sandbox Code Playgroud)

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

Main.cpp的

#include "Pimpl.h"

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

    pi->func_a(); // pointer semantics
    pi->func_b();
}
Run Code Online (Sandbox Code Playgroud)

  • 您可以使用`std :: unique_ptr <Interface> impl;`来保持PublicInterface整洁并提供异常安全性.这也将解决您现有的三条规则问题. (2认同)

Ton*_*roy 3

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

class X
{
  public:
    ...la de dah...
  private:
    struct Impl;
    Impl* p_impl_;
};
Run Code Online (Sandbox Code Playgroud)

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

  1. 您是否让成员函数定义实现了引用各处事物的X逻辑,或者p_impl_->

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

如果您选择 1. 那么您最终会在标头中得到一个函数声明,并在匹配的实现文件中得到一个(比平常稍微混乱的)定义。

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

通常没有可取的方法来改进这种情况(即构建过程中的宏黑客和额外的代码生成脚本有时可能是必要的,但很少见)。


整个堆可能并不重要,尽管第二种方法的变体可能是有趣的,即首先实现一个不使用pimpl 习惯用法的类(带有适当的标头和可选的内联函数),然后您可以包装它带有一个 pimpl 管理对象并将函数转发给它,这样您就可以自由地在某个地方拥有一些代码,有一天决定它想要使用该功能而不使用 pimpl 包装器,也许是为了提高性能/减少内存使用重新编译依赖项的成本。您还可以执行此操作以利用模板的特定实例化,而无需公开模板的代码。

为了说明这个选项(按照评论中的要求),让我们class X从它自己的文件中的一个愚蠢的非 pimpl 开始,然后创建一个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; }
}
Run Code Online (Sandbox Code Playgroud)

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