使用指针,引用,通用数据类型的句柄,尽可能通用和灵活

时间:2010-06-03 09:03:04

标签: c++ architecture generic-programming

在我的应用程序中,我有许多不同的数据类型,例如Car,Bicycle,Person,...(它们实际上是其他数据类型,但这只是用于示例)。

由于我的应用程序中也有一些“通用”代码,而且应用程序最初是用C编写的,因此指向Car,Bicycle,Person等的指针通常作为void指针传递给这些通用模块,具有类型的标识,如下所示:

Car myCar;
ShowNiceDialog ((void *)&myCar, DATATYPE_CAR);

'ShowNiceDialog'方法现在使用元信息(将DATATYPE_CAR映射到接口以从Car获取实际数据的函数),以根据给定的数据类型获取汽车的信息。这样,通用逻辑只需要编写一次,而不是每次都为每种新数据类型编写。

当然,在C ++中,您可以通过使用常见的根类来实现这一点,例如

class RootClass
   {
   public:
      string getName() const = 0;
   };

class Car : public RootClass
   {
   ...
   };

void ShowNiceDialog (RootClass *root);

问题在于,在某些情况下,我们不希望将数据类型存储在类中,而是以完全不同的格式存储以节省内存。 在某些情况下,我们需要在应用程序中管理数亿个实例,并且我们不希望为每个实例创建完整的类。 假设我们有一个具有2个特征的数据类型:

  • 数量(双倍,8个字节)
  • 布尔值(1个字节)

虽然我们只需要9个字节来存储这些信息,但是将它放在一个类中意味着我们需要至少16个字节(因为填充),而使用v指针我们甚至可能需要24个字节。 对于数亿个实例,每个字节都很重要(我有一个64位的应用程序变体,在某些情况下它需要6 GB的内存)。

void-pointer方法的优点是我们几乎可以在void-pointer中编码任何东西,如果我们想要它的信息,可以决定如何使用它(将它用作真正的指针,作为索引,......) ,但以类型安全为代价。

模板化解决方案没有帮助,因为通用逻辑构成了应用程序的很大一部分,我们不想将这一切模板化。此外,数据模型可以在运行时扩展,这也意味着模板无济于事。

有没有比void-pointer更好(和类型更安全)的方法来处理它? 有关这方面的框架,白皮书和研究材料的任何参考吗?

4 个答案:

答案 0 :(得分:3)

如果你不想要一个完整的课程,你应该阅读FlyWeight模式。它旨在节省内存。

编辑:对不起,午餐时间暂停;)

典型的FlyWeight方法是将大量对象共有的属性与给定实例的典型属性分开。

一般来说,这意味着:

struct Light
{
  kind_type mKind;
  specific1 m1;
  specific2 m2;
};

kind_type通常是一个指针,但是没有必要。在你的情况下,这将是一个真正的浪费,因为指针本身将是“有用”信息的4倍。

在这里,我认为我们可以利用填充来存储id。毕竟,正如你所说,它将扩展到16位,即使我们只使用其中的9位,所以让我们不要浪费其他7位!

struct Object
{
  double quantity;
  bool flag;
  unsigned char const id;
};

请注意元素的顺序很重要:

0x00    0x01    0x02    0x03
[      ][      ][      ][      ]
   quantity       flag     id

0x00    0x01    0x02    0x03
[      ][      ][      ][      ]
   id     flag     quantity

0x00            0x02            0x04
[      ][      ][      ][      ][      ][      ]
   id     --        quantity      flag     --

我不明白“在运行时扩展”位。似乎很吓人。这是某种自我修改的代码吗?

模板允许创建一个非常有趣的FlyWeight形式:Boost.Variant

typedef boost::variant<Car,Dog,Cycle, ...> types_t;

变体可以包含此处引用的任何类型。它可以通过“正常”函数进行操作:

void doSomething(types_t const& t);

可以存储在容器中:

typedef std::vector<types_t> vector_t;

最后,操作它的方式:

struct DoSomething: boost::static_visitor<>
{
  void operator()(Dog const& dog) const;

  void operator()(Car const& car) const;
  void operator()(Cycle const& cycle) const;
  void operator()(GenericVehicle const& vehicle) const;

  template <class T>
  void operator()(T const&) {}
};

注意这里的行为非常有趣。因此发生正常功能过载分辨率:

  • 如果您使用CarCycle,那么GenericVehicle的其他每个孩子都会使用第4版
  • 可以将模板版本指定为全部捕获,并适当指定。

我要注意,非模板方法可以在.cpp文件中完美定义。

要应用此访问者,请使用boost::apply_visitor方法:

types_t t;
boost::apply_visitor(DoSomething(), t);

// or

boost::apply_visitor(DoSomething())(t);

第二种方式似乎很奇怪,但这意味着你可以以最有趣的方式使用它,作为谓词:

vector_t vec = /**/;
std::foreach(vec.begin(), vec.end(), boost::apply_visitor(DoSomething()));

阅读变体,这是最有趣的。

  • 编译时间检查:您错过了一个operator()?编译器抛出
  • 不需要RTTI:没有虚拟指针,没有动态类型 - &gt;和使用联合一样快,但安全性提高

您当然可以通过定义多个变体来细分您的代码。如果代码的某些部分仅处理4/5类型,则使用特定的变体:)

答案 1 :(得分:2)

在这种情况下,听起来你应该只使用重载。例如:

#ifdef __cplusplus // Only enable this awesome thing for C++:
#   define PROVIDE_OVERLOAD(CLASS,TYPE) \
    inline void ShowNiceDialog(const CLASS& obj){ \ 
         ShowNiceDialog(static_cast<void*>(&obj),TYPE); \
    }

    PROVIDE_OVERLOAD(Car,DATATYPE_CAR)
    PROVIDE_OVERLOAD(Bicycle,DATATYPE_BICYCLE)
    // ...

#undef PROVIDE_OVERLOAD // undefine it so that we don't pollute with macros
#endif // end C++ only 

如果为各种类型创建重载,那么您将能够以简单且类型安全的方式调用ShowNiceDialog,但您仍然可以利用它的优化C变体。

使用上面的代码,您可以在C ++中编写如下内容:

 Car c;
 // ...
 ShowNiceDialog(c);

如果您更改了c的类型,那么它仍将使用适当的重载(或者如果没有重载则给出错误)。它并不妨碍使用现有的type-unsafe C变体,但由于类型安全版本更易于调用,我希望其他开发人员更喜欢它。

修改
我应该补充一点,上面回答了如何使API类型安全的问题,而不是如何使实现类型安全。这将有助于那些使用您的系统的人避免不安全的调用。还要注意,这些包装器提供了一种类型安全的方法来使用已经在编译时已知的类型...对于动态类型,它确实需要使用不安全的版本。但是,另一种可能性是您可以提供如下所示的包装类:

class DynamicObject
{
    public:
         DynamicObject(void* data, int id) : _datatype_id(id), _datatype_data(data) {}
         // ...
         void showNiceDialog()const{ ShowNiceDialog(_datatype_data,_datatype_id); }
         // ...
    private:
         int _datatype_id;
         void* _datatype_data;
};

对于那些动态类型,在构造对象时你仍然没有太多的安全性,但是一旦构造了对象,你就会有一个更安全的机制。将它与一个类型安全的工厂结合起来是合理的,这样你的API用户就不会自己实际构造DynamicObject类,因此不需要调用不安全的构造函数。

答案 2 :(得分:1)

完全可以在Visual Studio中更改类的包装 - 您可以使用__declspec(align(x))或#pragma pack(x),并且属性页中有一个选项。

我建议解决方案是将您的类分别存储在每个数据成员的向量中,然后每个类只包含对主类的引用和对这些向量的索引。如果主类是单身,那么这可以进一步改进。

class VehicleBase {
public:
    virtual std::string GetCarOwnerFirstName() = 0;
    virtual ~VehicleBase();
};
class Car : public VehicleBase {
    int index;
public:
    std::string GetCarOwnerFirstName() { return GetSingleton().carownerfirstnames[index]; }
};

当然,这留下了一些需要的实现细节,例如Car的数据成员的内存管理。然而,Car本身是微不足道的,可以随时创建/销毁,GetSingleton中的向量将非常有效地打包数据成员。

答案 3 :(得分:0)

我会使用特征

template <class T>
struct DataTypeTraits
{
};

template <>
struct DataTypeTraits<Car>
{
   // put things that describe Car here
   // Example: Give the type a name
   static std::string getTypeName()
   {
      return "Car";
   }
};
template <>
struct DataTypeTraits<Bicycle>
{
   // the same for bicycles
   static std::string getTypeName()
   {
      return "Bicycle";
   }
};

template <class T>
ShowNiceDialog(const T& t)
{
   // Extract details of given object
   std::string typeName(DataTypeTraits<T>::getTypeName());
   // more stuff
}

这样,无论何时添加要应用它的新类型,都不需要更改ShowNiceDialog()。您只需要为新类型的DataTypeTraits专门化。