保持c ++头文件之外的私有部分:纯虚拟基类与pimpl [英] keeping private parts outside c++ headers: pure virtual base class vs pimpl

查看:236
本文介绍了保持c ++头文件之外的私有部分:纯虚拟基类与pimpl的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

最近我从Java和Ruby切换回C ++,并且很奇怪,当我更改私有方法的方法签名时,我必须重新编译使用公共接口的文件,因为私有部分也在.h中文件。



我很快想出了一个解决方案,我猜想它对于Java程序员来说很典型:interfaces(=纯虚拟基本类)。例如:

BananaTree.h:

  class Banana; 

class BananaTree
{
public:
virtual Banana * getBanana(std :: string const& name)= 0;

static BananaTree * create(std :: string const& name);
};

BananaTree.cpp:

  class BananaTreeImpl:public BananaTree 
{
private:
字符串名称;

Banana * findBanana(string const& name)
{
return //获得香蕉,不知何故;

$ b public:
BananaTreeImpl(字符串名称)
:名称(名称)
{}

虚拟香蕉* getBanana(string const& name)
{
return findBanana(name);
}
};

BananaTree * BananaTree :: create(string const& name)
{
返回新BananaTreeImpl(name);
}

这里唯一的麻烦是我无法使用 new ,而必须调用 BananaTree :: create()。我不认为这真的是一个问题,尤其是因为我希望无论如何都会使用工厂。



现在,C ++名人的智者们来到了与另一种解决方案 pImpl idiom 联系起来。因此,如果我理解正确,我的代码将如下所示:



BananaTree.h:

  class BananaTree 
{
public:
Banana * addStep(std :: string const& name);

private:
struct Impl;
shared_ptr< Impl>的pimpl_;
};

BananaTree.cpp:

  struct BananaTree :: Impl 
{
string name;

Banana * findBanana(string const& name)
{
return //获得香蕉,不知何故;


Banana * getBanana(string const& name)
{
return findBanana(name);


Impl(string const& name):name(name){}
}

BananaTree :: BananaTree(string const& name)
:pimpl_(shared_ptr< Impl>(new Impl(name)))
{}

Banana * BananaTree :: getBanana(string const& name)
{
return pimpl _-> getBanana(name);
}

这意味着我必须为每个公共实现一个装饰器风格的转发方法方法 BananaTree ,在这种情况下 getBanana 。这听起来像是我不想要的复杂性和维护工作的额外水平。

所以,现在对于这个问题:纯虚拟类方法有什么问题?为什么pImpl方法有更好的文档记录?我错过了什么?

解决方案



使用虚拟基类可以打破一些人们对行为良好的C ++类的期望:



我期望(甚至要求)类可以在堆栈上实例化,如下所示:

  BananaTree myTree(somename); 

否则,我失去了RAII,我不得不手动开始跟踪分配,这导致了很多头痛和内存泄漏。

我也希望复制这个类,我可以简单地做到这一点。

  BananaTree tree2 = mytree; 

除非通过将复制构造函数标记为private来禁止复制,在这种情况下, t甚至可以编译。



在上面的例子中,我们明显遇到了这样的问题,即你的接口类没有真正有意义的构造函数。但是,如果我尝试使用上述示例的代码,那么我也会遇到很多切片问题。
使用多态对象时,通常需要持有指向对象的指针或引用,以防止切片。正如我的第一点,这通常是不可取的,并且使内存管理变得更加困难。

您的代码的读者会明白, BananaTree 基本上不起作用,他必须使用 BananaTree * BananaTree&

基本上,您的界面在现代C ++中表现不佳,我们更倾向于使用



  • 尽可能避免使用指针,并且
  • 堆栈分配所有对象,以从自动生命周期管理中受益。



顺便说一下,您的虚拟基类忘了虚拟析构函数。这是一个明确的错误。



最后,我有时使用一个简化的pimpl变体来减少样板代码的数量,以使外部对象可以访问内部对象的数据成员,所以您避免重复该接口。外部对象上的函数只是直接从内部对象访问它所需的数据,或者它调用内部对象上的辅助函数,而外部对象上没有相应的对象。



在你的例子中,你可以删除函数和 Impl :: getBanana ,而是实现 BananaTree :: getBanana >

  Banana * BananaTree :: getBanana(字符串常量&名称)
{
return pimpl _-> findBanana(name);
}

那么你只需要实现一个 getBanana 函数(在 BananaTree 类中)和一个 findBanana 函数(在 Impl class)。


I recently switched back from Java and Ruby to C++, and much to my surprise I have to recompile files that use the public interface when I change the method signature of a private method, because also the private parts are in the .h file.

I quickly came up with a solution that is, I guess, typical for a Java programmer: interfaces (= pure virtual base classes). For example:

BananaTree.h:

class Banana;

class BananaTree
{
public:
  virtual Banana* getBanana(std::string const& name) = 0;

  static BananaTree* create(std::string const& name);
};

BananaTree.cpp:

class BananaTreeImpl : public BananaTree
{
private:
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

public:
  BananaTreeImpl(string name) 
    : name(name)
  {}

  virtual Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }
};

BananaTree* BananaTree::create(string const& name)
{
  return new BananaTreeImpl(name);
}

The only hassle here, is that I can't use new, and must instead call BananaTree::create(). I do not think that that is really an problem, especially since I expect to be using factories a lot anyway.

Now, the wise men of C++ fame, however, came up with another solution, the pImpl idiom. With that, if I understand it correctly, my code would look like:

BananaTree.h:

class BananaTree
{
public:
  Banana* addStep(std::string const& name);

private:
  struct Impl;
  shared_ptr<Impl> pimpl_;
};

BananaTree.cpp:

struct BananaTree::Impl
{
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

  Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }

  Impl(string const& name) : name(name) {}
}

BananaTree::BananaTree(string const& name)
  : pimpl_(shared_ptr<Impl>(new Impl(name)))
{}

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->getBanana(name);
}

This would mean I have to implement a decorator-style forwarding method for every public method of BananaTree, in this case getBanana. This sounds like an added level of complexity and maintenance effort that I prefer not to require.

So, now for the question: What is wrong with the pure virtual class approach? Why is the pImpl approach so much better documented? Did I miss anything?

解决方案

I can think of a few differences:

With the virtual base class you break some of the semantics people expect from well-behaved C++ classes:

I would expect (or require, even) the class to be instantiated on the stack, like this:

BananaTree myTree("somename");

otherwise, I lose RAII, and I have to manually start tracking allocations, which leads to a lot of headaches and memory leaks.

I also expect that to copy the class, I can simply do this

BananaTree tree2 = mytree;

unless of course, copying is disallowed by marking the copy constructor private, in which case that line won't even compile.

In the above cases, we obviously have the problem that your interface class doesn't really have meaningful constructors. But if I tried to use code such as the above examples, I'd also run afoul of a lot of slicing issues. With polymorphic objects, you're generally required to hold pointers or references to the objects, to prevent slicing. As in my first point, this is generally not desirable, and makes memory management much harder.

Will a reader of your code understand that a BananaTree basically doesn't work, that he has to use BananaTree* or BananaTree& instead?

Basically, your interface just doesn't play that well with modern C++, where we prefer to

  • avoid pointers as much as possible, and
  • stack-allocate all objects to benefit form automatic lifetime management.

By the way, your virtual base class forgot the virtual destructor. That's a clear bug.

Finally, a simpler variant of pimpl that I sometimes use to cut down on the amount of boilerplate code is to give the "outer" object access to the data members of the inner object, so you avoid duplicating the interface. Either a function on the outer object just accesses the data it needs from the inner object directly, or it calls a helper function on the inner object, which has no equivalent on the outer object.

In your example, you could remove the function and Impl::getBanana, and instead implement BananaTree::getBanana like this:

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->findBanana(name);
}

then you only have to implement one getBanana function (in the BananaTree class), and one findBanana function (in the Impl class).

这篇关于保持c ++头文件之外的私有部分:纯虚拟基类与pimpl的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆