【问题标题】:How to achieve logical constness with the PIMPL idiom如何使用 PIMPL 成语实现逻辑常量
【发布时间】:2016-09-16 09:37:57
【问题描述】:

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

class ObjectImpl;

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

private:      
  ObjectImpl* _impl;
};

我正在寻找的是一种重用相同实现来包装类型 T 的方法,该类型 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:
};

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

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

【问题讨论】:

  • 在构造函数中采用实现指针的 PIMPL 没有什么典型之处。 PIMPL 应该是一个实现细节,在类的接口上是不可见的。
  • 该构造函数可以是私有的,用于内部目的

标签: c++11 constants pimpl-idiom


【解决方案1】:

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 的实现。由于它只涉及将内容转发给实际的 impl,因此这似乎相对无害。

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

另一种有点疯狂的方法是使用带有类型擦除操作的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-&gt;*dance)()。我们的const_object co; 可以double d = (co-&gt;*boogie)();

任何支持printboogiedance 描述的操作以及any 要求(复制、销毁、分配)的东西都可以存储在object 中。什么都可以。

同样,const_object 支持printboogie 以及复制/销毁/分配可以描述的任何内容。

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

这项技术很先进。你可以使用boost::type_erasure 来做,可能比这个草图更流畅。

【讨论】:

  • 希望我没有打扰你,但我能问一下this trivial solution 有什么问题吗?非常感谢。
  • @AmiTavory 您不能从const ObjectImpl 构造ConstObject。而且你缺少像operator ConstObject 这样的东西(没有基本的东西)。就是这样。
  • 题主也担心带模板的DLL接口导出。 CRTP 是否也解决了这个问题?
  • @Aurelien 将导出两个模板实例——const_Object_helper&lt;const_Object&gt; const_Object_helper&lt;Object&gt; 。在 Windows 上可以从 DLL 中手动导出两个模板(我已经做到了)。
【解决方案2】:

我建议采用以下通用设计模式。它浪费了一个额外的指针,但会强制要求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;
};

【讨论】:

  • 我的想法非常相似,比如像往常一样实现 Object,然后执行 class ConstObject : public Object { private: const ObjectImpl* _impl; } 遮蔽基类成员。虽然看起来有点hacky和丑陋。您的解决方案肯定看起来更好。
  • 这复制了指针。这违反了 DRY,使用资源,并打开了一大堆可能的错误。例如 Object foo(blah); const_Object bar(newt); const_Object&amp; foo_const=foo; foo_const=bar; 刚刚切片 foo 并使其指向两个 pImpl。
猜你喜欢
  • 2011-01-14
  • 1970-01-01
  • 2022-09-25
  • 1970-01-01
  • 1970-01-01
  • 2017-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多