如何使用 PIMPL 习语实现逻辑常量

How to achieve logical constness with the PIMPL idiom

想象一下 PIMPL 习语的典型实现:

class ObjectImpl;

class Object
{
  Object(ObjectImpl* object_impl)
    : _impl(object_impl);

private:      
  ObjectImpl* _impl;
};

我正在寻找的是一种重用相同实现来包装类型 T 的方法,它可以是 ObjectImpl 或 const ObjectImpl,但没有别的:

class ObjectImpl;

class Object
{
  Object(T* object_impl)
    : _impl(object_impl);

private:
  // T being either ObjectImpl or const ObjectImpl
  T* _impl;
};

我想要实现的是通过 PIMPL 接口保留逻辑常量,这样编译器就不允许我在包装 const ObjectImpl* 的对象上调用非常量方法。

这基本上只是从 Scott Meyers Effective C++ 书中借用的技巧,但增加了抽象层:

struct SData
{
  const Data* data() const { return _data; }
  Data* data() { return _data; }

private:
  Data* _data:
};

当然我可以将整个 class 复制到一个 class ConstObject 中并让它包装一个 const* Object 而不是 Object* 但我显然是在试图防止代码重复。

我也考虑过模板,但它们对于手头的任务来说似乎有点过分了。首先,我希望 T 只能是 ObjectImpl 或 const ObjectImpl。其次,模板在导出为 DLL 接口时似乎与 PIMPL 的想法背道而驰。有更好的解决方案吗?

我建议采用以下通用设计模式。它浪费了一个额外的指针,但会强制要求 const 对象只能访问私有对象的 const 方法:

class ObjectImpl;

class const_Object {

public:

  const_Object(const ObjectImpl* object_impl)
    : _impl(object_impl);

  // Only const methods

private:      
  const ObjectImpl* _impl;
};

class Object : public const_Object
{
  Object(ObjectImpl* object_impl)
    : const_Object(object_impl), _impl(object_impl);

  // non-const methods go here.

private:      
  ObjectImpl* _impl;
};

CRTP.

template<class Storage>
struct const_Object_helper {
  Storage* self() { return static_cast<D*>(this); }
  Storage const* self() const { return static_cast<D*>(this); }
  // const access code goes here, get it via `self()->PImpl()`
};
struct const_Object: const_Object_helper<const_Object> {
  const_Object( objectImpl const* impl ):pImpl(impl) {}
private:
  objectImpl const* pImpl = nullptr;
  objectImpl const* PImpl() const { return pImpl; }
  template<class Storage>
  friend struct const_Object_helper;
};
struct Object: const_Object_helper<Object> {
  // put non-const object code here
  Object( objectImpl* impl ):pImpl(impl) {}
  operator const_Object() const {
    return {PImpl()}; // note, a copy/clone/rc increase may be needed here
  }
private:
  objectImpl* pImpl = nullptr;
  objectImpl const* PImpl() const { return pImpl; }
  objectImpl* PImpl() { return pImpl; }
  template<class Storage>
  friend struct const_Object_helper;
};

这是零运行时开销版本,但需要公开 const_Object_helperObject_helper 的实现。因为它只涉及将东西转发给实际的实现,所以这似乎相对无害。

您可以通过用纯虚拟 objectImpl const* get_pimpl() const = 0objectImpl* get_pimpl() = 0 替换助手的 CRTP 部分来消除这种需要,然后在派生类型中实现它们。

另一种有点疯狂的方法是使用 any 增强类型擦除操作(您还想教授有关 const 的类型擦除机制,并且 super_any 更少的接口可以隐式转换而不做另一层包装)。

这里我们定义了某些操作,比如打印、跳舞和布吉:

auto const print = make_any_method<void(std::ostream&), true>(
  [](auto&&self, std::ostream& s) {
    s << decltype(self)(self);
  }
);
auto const dance = make_any_method<void()>(
  [](auto&&self) {
    decltype(self)(self).dance();
  }
);
auto const dance = make_any_method<double(), true>(
  [](auto&&self) {
    return decltype(self)(self).boogie();
  }
);

现在我们创建两种类型:

using object = super_any< decltype(print), decltype(dance), decltype(boogie) > object;
using const_object = super_any< decltype(print), decltype(boogie) >;

接下来,增加 super_any 从具有严格较弱要求的来源分配的能力。

我们的object o;可以(o->*dance)()。我们的const_object co;可以double d = (co->*boogie)();.

支持 printboogiedance 描述的操作的任何内容都可以存储在 object 中,加上 any 的要求(复制、销毁、分配)。什么都行。

同样,const_object 支持任何可以用 printboogie 和 copy/destroy/assign 描述的内容。

来自 objectconst_object 的派生类型可以轻松添加运算符重载功能。

这项技术很先进。您可以使用 boost::type_erasure 来完成,可能比这个草图更圆滑。