C++ 当您';I’我希望接口有所有的内存

C++ 当您';I’我希望接口有所有的内存,c++,C++,PIMPL习惯用法的目的是隐藏实现,包括方法、结构甚至结构的大小。一个缺点是它使用堆 但是,如果我不想隐藏任何东西的尺寸要求,该怎么办。我只是想隐藏方法、结构的格式和变量名。一种方法是分配一个完美大小的字节数组,让实现不断地将其转换为任何结构并使用它。但是手动查找要为对象分配的字节大小?你一直在做演员吗?显然不实际 是否有一种惯用方法或通用方法来处理这种情况,从而有利于PIMPL或不透明指针。您试图隐藏的信息与编译器计算大小所需的信息完全相同。也就是说,不,在不知道非静态成员的数量和数据类型的情

PIMPL习惯用法的目的是隐藏实现,包括方法、结构甚至结构的大小。一个缺点是它使用堆

但是,如果我不想隐藏任何东西的尺寸要求,该怎么办。我只是想隐藏方法、结构的格式和变量名。一种方法是分配一个完美大小的字节数组,让实现不断地将其转换为任何结构并使用它。但是手动查找要为对象分配的字节大小?你一直在做演员吗?显然不实际


是否有一种惯用方法或通用方法来处理这种情况,从而有利于PIMPL或不透明指针。

您试图隐藏的信息与编译器计算大小所需的信息完全相同。也就是说,不,在不知道非静态成员的数量和数据类型的情况下,不存在查找大小的习惯用法,因为这甚至是不可能的

另一方面,可以很好地隐藏辅助函数的存在。只需声明一个嵌套类型(这使嵌套成员可以访问外部类的私有成员),并仅在私有实现文件中定义该类型,将助手逻辑放入嵌套类型的静态成员函数中。您必须传递一个指向对象实例的指针作为参数进行操作,然后才能访问所有成员

例如:

class FlatAPI
{
    void helperNeedsPublicAccess();
    void helperNeedsFullAccess();

    T data;
public:
    void publicFunction();
};
变成

class PublicAPI
{
    struct helpers;

    T data;
public:
    void publicFunction();
};
和实现代码

#include <public.h>

static void helperNeedsPublicAccess(PublicAPI* pThis) { pThis->publicFunction(); }

struct PublicAPI::helpers
{
    static void helperNeedsFullAccess(PublicAPI* pThis) { std::cout << pThis->data; }
};

void PublicAPI::publicFunction()
{
    helpers::helperNeedsFullAccess(this);
}
#包括
静态void helperNeedsPublicAccess(PublicAPI*pThis){pThis->publicFunction();}
struct PublicAPI::helpers
{
静态void helpernedsfullaccess(PublicAPI*pThis){std::cout data;}
};
void PublicAPI::publicFunction()
{
helpers::helpernedsfullaccess(this);
}

一种完全不同的方法是重新思考对象真正代表的本质。在传统的OOP中,习惯上认为所有对象都是具有自己的数据和方法的自包含实体。其中一些方法对于类来说是私有的,因为它们只是该类自身的内务管理所必需的,所以这些方法通常是您移动Pimpl类的“impl”的

在最近的一个项目中,我一直支持这样一种方法,其中一个可取之处是将数据与处理数据的逻辑分离。然后,数据类就变成了结构,以前隐藏在Pimpl中的复杂逻辑现在可以放在一个没有自己状态的服务对象中

考虑一个(相当人为的)游戏循环示例:

class EnemySoldier : public GameObject
{
public:
    // just implement the basic GameObject interface
    void        updateState();
    void        draw(Surface&);

private:
    std::unique_ptr<EnemySoldierImp>  m_Pimpl;
};
可以对其进行重组,这样我们就不必管理游戏对象的数据和程序逻辑,而是将这两件事分开:

class EnemySoldierData
{
public:
    // some getters may be allowed, all other data only 
    // modifiable by the Service class. No program logic in this class
private:
    friend class EnemySoldierAIService;
    StateData       m_StateData;
};
我们现在不需要PIMPL或任何关于内存分配的黑客把戏。我们还可以使用游戏编程技术,通过将全局状态存储在几个平面向量中,而不需要指向基类的指针数组,从而获得更好的缓存性能并减少内存碎片,例如:

class Game
{
public:
        std::vector<EnemySoldierData> m_SoldierData;
        std::vector<MissileData>     m_MissileData;
        ...
}
类游戏
{
公众:
std::向量m_士兵数据;
std::向量m_MissileData;
...
}
我发现这种通用方法确实简化了许多程序代码:

  • 不再需要皮条客了
  • 程序逻辑都在一个地方
  • 通过在运行时在服务类的V1和V2版本之间进行选择,可以更容易地保持向后兼容性或插入替代实现
  • 更不用说堆分配了

因此,这里有一个可能的替代方案,它没有持续强制转换的缺点,但改进了内存布局,使其与根本没有使用PIMPL的情况类似

我将假设您的应用程序实际上并不是只使用一个pimpl,但实际上您对许多类使用pimpl,所以它就像,第一个pimpl的impl对许多子类持有pimpl,而那些pimpl的impl对许多第三层类持有pimpl等等

(我所设想的对象类型包括应用程序中的所有管理器、调度器和各种引擎。很可能不是所有的实际数据记录,它们可能位于其中一个管理器拥有的标准容器中。但在应用程序过程中,您通常只拥有固定数量的所有对象。)

第一个想法是,类似于
std::make_shared
的工作原理,我希望将主对象分配到“helper”对象旁边,这样我就可以在不破坏封装的情况下获得“快速”内存布局。我这样做的方式是分配一个足够大的连续内存块,并使用placement new,这样pimpl就在impl旁边

就其本身而言,这并不是什么真正的改进,因为pimpl只是一个指针的大小,任何拥有pimpl的人现在都需要一个指向pimpl的指针,因为它现在是堆分配的

但是,现在我们基本上尝试同时对所有层执行此操作

要真正做到这一点,需要什么:

  • 每个pimpl类都需要公开一个静态成员函数,该函数在运行时可用,以字节表示其大小。如果对应的impl很简单,那么它可能只是返回sizeof(my_impl)。如果对应的impl包含其他PIMPL,则这是
    返回sizeof(my_impl)+child_pimpl1::size()+child_pimpl2::size()+…
  • 每个pimpl类都需要一个自定义的
    运算符new
    或类似的工厂函数,该函数将分配给适当大小的给定内存块
    • pimpl及其impl(减去递归处理的pimpl子项)
    • 每个pimpl子项使用其相应的
      运算符new
      或类似函数连续运行
现在,在
class EnemySoldierData
{
public:
    // some getters may be allowed, all other data only 
    // modifiable by the Service class. No program logic in this class
private:
    friend class EnemySoldierAIService;
    StateData       m_StateData;
};
class EnemySoldierAIService
{
public:
    EnemySoldierAIService() {}

    void updateState(Game& game) {
        for (auto& enemySoldierData : game.getAllEnemySoldierData()) {
            updateStateForSoldier(game, enemySoldierData);
        }
    }

    // 100 methods of AI logic are now here

    // no state variables
};
class Game
{
public:
        std::vector<EnemySoldierData> m_SoldierData;
        std::vector<MissileData>     m_MissileData;
        ...
}
#include <iostream>
#include <sstream>
#include <vector>
#include <cassert>

  // ///////////////////////////////////////////////////////////////////////
  // ///////////////////////////////////////////////////////////////////////
  // file Foo.hh
  class Foo     // a pimple example
  {
  public:
     Foo();
     ~Foo();

     // alternative for above two methods: use named ctor/dtor

     // diagnostics only
     std::string show();

     // OTHER METHODS not desired

  private:
     // pathological dependency 1 - manual guess vs actual size
     enum SizeGuessEnum { SizeGuess = 24048 };

     char implBuff [SizeGuess]; // space inside Foo object to hold FooImpl
     // NOTE - this is _not_ an allocation - it is _not_ new'd, so do not delete

     // optional: declare the name of the class/struct to hold Foo attributes
     //   this is only a class declaration, with no implementation info
     //   and gives nothing away with its name
     class FooImpl;

     // USE RAW pointer only, DO NOT USE any form of unique_ptr
     // because pi does _not_ point to a heap allocated buffer
     FooImpl*   pi;  // pointer-to-implementation
  };





  // ///////////////////////////////////////////////////////////////////////
  // ///////////////////////////////////////////////////////////////////////
  // top of file Foo.cc

  typedef std::vector<std::string> StringVec;


  // the impl defined first
  class Foo::FooImpl
  {
  private:
     friend class Foo;  // allow Foo full access

     FooImpl() : m_indx(++M_indx)
        {
           std::cout << "\n   Foo::FooImpl() sizeof() = "
                     << sizeof(*this); // proof this is accessed
        }

     ~FooImpl()  { m_indx = 0; }


     uint64_t  m_indx;  // unique id for this instance

     StringVec m_stringVec[1000]; // room for 1000 strings

     static uint64_t M_indx;
  };

  uint64_t Foo::FooImpl::M_indx = 0;  // allocation of static



  // Foo ctor
  Foo::Foo(void) : pi (nullptr)
  {
     // pathological dependency 1 - manual guess vs actual size
     {
        // perform a one-time run-time VALIDATE of SizeGuess

        // get the compiler's actual size
        const size_t ActualSize = sizeof(FooImpl);

        // SizeGuess must accomodate entire FooImpl
        assert(SizeGuess >= ActualSize);

        // tolerate some extra buffer - production code might combine above with below to make exact
        // SizeGuess can be a little bit too big, but not more than 10 bytes too big
        assert(SizeGuess <= (ActualSize+10));
     }

     // when get here, the implBuff has enough space to hold a complete  Foo::FooImpl

     // some might say that the following 'for loop' would cause undefined behavior
     // by treating the code differently than subsequent usage
     // I think it does not matter, so I will skip
     {
        // 0 out the implBuff
        // for (int i=0; i<SizeGuess; ++i) implBuff[i] = 0;
     }

     // pathological dependency 2 - use of placement new
     //   --> DOES NOT allocate heap space  (so do not deallocate in dtor)
     pi = new (implBuff) FooImpl();

     // NOTE: placement new does not allocate, it only runs the ctor at the address
     //       confirmed by cout of m_indx
  }

  Foo::~Foo(void)
  {
     // pathological dependency 2 - placement new DOES NOT allocate heap space
     // DO NOT delete what pi points to


     // YOU MAY perform here the actions you think are needed of the FooImpl dtor
     // or
     // YOU MAY write a FooImpl.dtor and directly invoke it (i.e. pi->~FooImpl() )
     //
     // BUT -- DO NOT delete pi, because FOO did not allocate *pi
  }


  std::string Foo::show() // for diagnostics only
  {
     // because foo is friend class, foo methods have direct access to impl
     std::stringstream ss;
     ss << "\nsizeof(FooImpl): " << sizeof(FooImpl)
        << "\n      SizeGuess: " << SizeGuess
        << "\n           this: " << (void*) this
        << "\n      &implBuff: " << &implBuff
        << "\n     pi->m_indx: " << pi->m_indx;
     return (ss.str());
  }


int t238(void) // called by main
{
   {
      Foo foo;
      std::cout << "\n   foo on stack: " << sizeof(foo) << " bytes";
      std::cout <<  foo.show() << std::endl;
   }

   {
      Foo* foo = new Foo;

      std::cout << "\nfoo ptr to Heap: " << sizeof(foo) << " bytes";
      std::cout << "\n    foo in Heap: " << sizeof(*foo) << " bytes";
      std::cout <<  foo->show() << std::endl;

      delete foo;
   }

   return (0);
}
// output
//    Foo::FooImpl() sizeof() = 24008
//    foo on stack: 24056 bytes
// sizeof(FooImpl): 24008
//       SizeGuess: 24048
//            this: 0x7fff269e37d0
//       &implBuff: 0x7fff269e37d0
//      pi->m_indx: 1
//
//    Foo::FooImpl() sizeof() = 24008
// foo ptr to Heap: 8 bytes
//     foo in Heap: 24056 bytes
// sizeof(FooImpl): 24008
//       SizeGuess: 24048
//            this: 0x1deffe0
//       &implBuff: 0x1deffe0
//      pi->m_indx: 2