4

在我的应用程序中,我有很多不同的数据类型,例如 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-pointer 我们甚至可能需要 24 个字节。对于数亿个实例,每个字节都很重要(我有一个 64 位的应用程序变体,在某些情况下它需要 6 GB 的内存)。

void-pointer 方法的优点是我们几乎可以在 void-pointer 中编码任何内容,并决定如果我们想要从中获取信息(将其用作真正的指针、索引等)如何使用它,但在类型安全的成本。

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

有没有比空指针更好(和类型更安全)的方法来处理这个问题?有没有关于这方面的框架、白皮书、研究材料的参考?

4

4 回答 4

3

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

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

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

一般来说,它的意思是:

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

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

在这里,我认为我们可以利用填充来存储 id。毕竟,正如你所说,即使我们只使用其中的 9 个,它也会扩展到 16 位,所以我们不要浪费其他 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&) {}
};

注意这里的行为非常有趣。发生正常的函数重载决议,因此:

  • 如果你有一个Car或一个Cycle你会使用那些,每个其他的孩子都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:没有虚拟指针,没有动态类型 --> 与使用联合一样快,但安全性更高

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

于 2010-06-03T09:50:12.067 回答
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,那么它仍然会使用适当的重载(如果没有重载,则会给出错误)。它不会阻止人们使用现有的类型不安全的 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 类,因此不需要调用不安全的构造函数。

于 2010-06-03T09:16:10.833 回答
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]; }
};

Of course, this leaves some implementation details to be desired, such as the memory management of Car's data members. However, Car itself is trivial and can be created/destroyed at any time, and the vectors in GetSingleton will pack data members quite efficiently.

于 2010-06-03T12:14:24.997 回答
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 特化。

于 2010-06-03T09:30:38.667 回答