Использование указателей, ссылок, дескрипторов общих типов данных, как можно более универсальных и гибких - PullRequest
4 голосов
/ 03 июня 2010

В моем приложении много разных типов данных, например, 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 характеристиками:

  • Количество (double, 8 байтов)
  • логическое (1 байт)

Хотя нам нужно только 9 байтов для хранения этой информации, размещение ее в классе означает, что нам нужно как минимум 16 байтов (из-за заполнения), а с v-указателем нам, возможно, даже понадобится 24 байта. Для сотен миллионов экземпляров учитывается каждый байт (у меня есть 64-битный вариант приложения, а в некоторых случаях требуется 6 ГБ памяти).

Преимущество подхода void-pointer заключается в том, что мы можем почти кодировать что-либо в void-указатель и решать, как его использовать, если мы хотим получить от него информацию (использовать его как реальный указатель, как индекс, ...) , но за счет безопасности типа.

Шаблонные решения не помогают, так как общая логика составляет довольно большую часть приложения, и мы не хотим все это шаблонизировать. Кроме того, модель данных может быть расширена во время выполнения, что также означает, что шаблоны не помогут.

Существуют ли лучшие (и безопасные для типов) способы справиться с этим, чем указатель void? Любые ссылки на рамки, технические документы, исследовательские материалы по этому поводу?

Ответы [ 4 ]

3 голосов
/ 03 июня 2010

Если вам не нужен полный урок, вам следует ознакомиться с шаблоном FlyWeight .Он предназначен для экономии памяти.

РЕДАКТИРОВАТЬ: извините, пауза во время обеда;)

Типичный подход FlyWeight заключается в разделении свойств, которые являются общими для большого числаобъектов из свойств, которые типичны для данного экземпляра.

Как правило, это означает:

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

kind_type часто является указателем, однако в этом нет необходимости.В вашем случае это будет реальная трата, потому что сам указатель будет в 4 раза больше «полезной» информации.

Здесь я думаю, что мы могли бы использовать заполнение для хранения идентификатора.В конце концов, как вы сказали, он будет расширен до 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 будет использовать 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 типами, используйте для этого определенный вариант:)

2 голосов
/ 03 июня 2010

В этом случае кажется, что вы должны просто использовать перегрузку. Например:

#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 ++ написать что-то вроде следующего:

 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 сами, и поэтому не нужно было бы вызывать небезопасный конструктор.

1 голос
/ 03 июня 2010

Вполне возможно изменить упаковку класса, скажем, в 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 будут достаточно эффективно упаковывать элементы данных.

0 голосов
/ 03 июня 2010

Я бы использовал черты

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 для нового типа.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...