代码之家  ›  专栏  ›  技术社区  ›  Patrick

尽可能灵活地使用泛型数据类型的指针、引用、句柄

  •  4
  • Patrick  · 技术社区  · 15 年前

    在我的应用程序中,我有很多不同的数据类型,例如汽车,自行车,人(它们实际上是其他数据类型,但这只是示例)。

    因为我的应用程序中也有相当多的“通用”代码,而且应用程序最初是用C编写的,指向汽车、自行车、人的指针。。。通常作为空指针传递给这些泛型模块,以及类型标识,如下所示:

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

    当然,在C++中,使用通用的根类可以使这更容易。

    class RootClass
       {
       public:
          string getName() const = 0;
       };
    
    class Car : public RootClass
       {
       ...
       };
    
    void ShowNiceDialog (RootClass *root);
    

    问题是,在某些情况下,我们不希望将数据类型存储在类中,而是以完全不同的格式来保存内存。 在某些情况下,我们需要在应用程序中管理数以亿计的实例,并且我们不想为每个实例创建一个完整的类。

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

    尽管我们只需要9个字节来存储这些信息,但将其放入一个类中意味着我们至少需要16个字节(由于填充),而对于v指针,我们甚至可能需要24个字节。 对于数以亿计的实例,每个字节都会计数(我有一个64位的应用程序变体,在某些情况下它需要6gb的内存)。

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

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

    4 回复  |  直到 15 年前
        1
  •  3
  •   Matthieu M.    15 年前

    如果你不想整堂课,你应该好好读一读 FlyWeight 图案。它是为了节省内存而设计的。

    编辑:抱歉,午餐时间暂停;)

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

    这个 kind_type

    在这里,我认为我们可以利用填充来存储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&) {}
    };
    

    注意到这里的行为非常有趣。出现正常的函数重载解析,因此:

    • Car 或者 Cycle 你会用这些,每一个孩子 GenericVehicle 我们要第四版吗
    • 可以将模板版本指定为catch-them-all,并适当地指定它。

    为了应用此访问者,您可以使用 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类型,则使用特定的变体:)

        2
  •  2
  •   Michael Aaron Safyan    15 年前

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

    #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;
    };
    

    对于那些动态类型,在构建对象时仍然没有太多安全性,但是一旦构建了对象,就有了更安全的机制。将其与typesafe工厂相结合是合理的,这样API的用户就永远不会自己构造DynamicObject类,也就不需要调用不安全的构造函数。

        3
  •  1
  •   Puppy    15 年前

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

    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中的向量将非常有效地打包数据成员。

        4
  •  0
  •   jopa    15 年前

    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。