Как хранить разные типы данных в одном списке? (C ++)

Мне нужно хранить список различных свойств объекта. Свойство состоит из имени и данных, которые могут иметь любой тип данных.

Я знаю, что могу создать класс «Свойство» и расширить его различными классами PropertySubClasses, которые отличаются только типом данных, который они хранят, но он не чувствует

class Property
{
     Property(std::string name);
     virtual ~Property();

     std::string m_name;
};

class PropertyBoolean : Property
{
     PropertyBoolean(std::string name, bool data);

     bool m_data;
};

class PropertyFloat : Property
{
     PropertyFloat(std::string name, float data);

     float m_data;
};

class PropertyVector : Property
{
     PropertyVector(std::string name, std::vector<float> data);

     std::vector<float> m_data;
};

Теперь я могу хранить все виды свойств в

 std::vector<Property*>

и, чтобы получить данные, я могу привести объект к подклассу. Или я могу сделать чисто виртуальную функцию, чтобы что-то делать с данными внутри функции без необходимости приведения.

В любом случае, это неправильно для создания этих различных подклассов, которые отличаются только типом данных, которые они хранят. , Есть ли другой удобный способ добиться подобного поведения?

У меня нет доступа к Boost.

21
задан hasdf 24 August 2010 в 18:01
поделиться

7 ответов

C ++ - это многопарадигмальный язык. Он наиболее ярко проявляется и наиболее действенен там, где смешиваются парадигмы.

class Property
{
public:
    Property(const std::string& name) //note: we don't lightly copy strings in C++
      : m_name(name) {}
    virtual ~Property() {}
private:
    std::string m_name;
};

template< typename T >
class TypedProperty : public Property
{
public:
    TypedProperty (const std::string& name, const T& data)
      : Property(name), m_data(data);
private:
    T m_data;
};

typedef std::vector< std::shared_ptr<Property> > property_list_type;

Изменить: Почему используется std :: shared_ptr вместо Свойство * ?
Рассмотрим этот код:

void f()
{
  std::vector<Property*> my_property_list;
  for(unsigned int u=0; u<10; ++u)
    my_property_list.push_back(new Property(u));

  use_property_list(my_property_list);

  for(std::vector<Property*>::iterator it=my_property_list.begin();
                                      it!=my_property_list.end(); ++it)
    delete *it;
}

Этот цикл for пытается очистить, удаляя все свойства в векторе, непосредственно перед тем, как он выйдет за пределы области видимости и заберет с собой все указатели.
Хотя для новичка это может показаться нормальным, но если вы - лишь слегка опытный разработчик C ++, этот код должен вызвать тревогу, как только вы на него посмотрите.

Проблема в том, что вызов use_property_list () может вызвать исключение. В этом случае функция f () сразу же будет оставлена. Для правильной очистки будут вызваны деструкторы для всех автоматических объектов, созданных в f () . То есть my_property_list будет правильно уничтожен. Затем деструктор std :: vector аккуратно очистит содержащиеся в нем данные. Однако он содержит указателей , и как std :: vector узнать, являются ли эти указатели последними, ссылающимися на их объекты?
Поскольку он не знает, он не будет удалять объекты, он уничтожит указатели только тогда, когда уничтожит его содержимое, оставив вам объекты в куче, на которые у вас больше нет указателей. Это то, что называется «утечкой».

Чтобы этого избежать, вам нужно будет перехватить все исключения, очистить свойства и повторно выбросить исключение. Но затем, через десять лет, кто-то должен добавить новую функцию в приложение 10MLoC, до которого оно выросло, и, торопясь, добавить код, который преждевременно покидает эту функцию, когда выполняется какое-либо условие. Код протестирован, работает и не дает сбоев - только сервер, частью которого он является, теперь пропускает несколько байтов в час, что приводит к сбою из-за нехватки памяти примерно раз в неделю. Обнаружение этого делает многие часы прекрасной отладкой.

Итог: никогда не управляйте ресурсами вручную, всегда оборачивайте их в объекты класса, предназначенные для обработки ровно одного экземпляра такого ресурса. Для динамически выделяемых объектов эти дескрипторы называются «интеллектуальным указателем» , а наиболее часто используемый - shared_ptr .

28
ответ дан 29 November 2019 в 20:16
поделиться

Я предлагаю boost :: variant или boost :: any . [ Связанный вопрос ]

4
ответ дан 29 November 2019 в 20:16
поделиться

Я вижу, что есть много попыток чтобы решить вашу проблему к настоящему моменту, но у меня такое чувство, что вы смотрите не в ту сторону - почему вы на самом деле вообще хотите это сделать? Есть ли в базовом классе какие-то интересные функции, которые вы не указали?

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

1
ответ дан 29 November 2019 в 20:16
поделиться

Вы, вероятно, можете сделать это с помощью библиотеки Boost, или вы могли бы создать класс с кодом типа и указателем void на данные, но это означало бы отказ от некоторой типовой безопасности C ++ . Другими словами, если у вас есть свойство «foo», значение которого является целым числом, и вы вместо этого присваиваете ему строковое значение, компилятор не найдет для вас ошибку.

Я бы порекомендовал пересмотреть ваш дизайн и переоценить, действительно ли вам нужна такая гибкость. Вам действительно нужно иметь возможность обрабатывать свойства любого типа? Если вы можете сузить его до нескольких типов, вы сможете найти решение, используя наследование или шаблоны, без необходимости «бороться с языком».

0
ответ дан 29 November 2019 в 20:16
поделиться

Другое возможное решение - написать промежуточный класс, управляющий указателями на классы Property :

class Bla {
private:
  Property* mp
public:
  explicit Bla(Property* p) : mp(p) { }

  ~Bla() { delete p; }

  // The standard copy constructor
  // and assignment operator
  // aren't sufficient in this case:
  // They would only copy the 
  // pointer mp (shallow copy)
  Bla(const Bla* b) : mp(b.mp->clone()) { }

  Bla& operator = (Bla b) { // copy'n'swap trick
    swap(b);
    return *this;
  }

  void swap(Bla& b) {
    using std::swap; // #include <algorithm>
    swap(mp, b.mp);
  }

  Property* operator -> () const {
    return mp;
  }

  Property& operator * () const {
    return *mp;
  }
};

Вы должны добавить виртуальный метод clone в ваши классы возвращают указатель на только что созданную копию самого себя:

class StringProperty : public Property {
// ...
public:
  // ...
  virtual Property* clone() { return new StringProperty(*this); }
  // ...
};

Затем вы сможете сделать это:

std::vector<Bla> v;
v.push_back(Bla(new StringProperty("Name", "Jon Doe")));
// ...
std::vector<Bla>::const_iterator i = v.begin();
(*i)->some_virtual_method();

Выход из области видимости v означает, что все Bla s будет уничтожен, автоматически освободив указатели, которые они содержат. Из-за перегруженного оператора разыменования и косвенного обращения класс Bla ведет себя как обычный указатель. В последней строке * i возвращает ссылку на объект Bla , а использование -> означает то же самое, как если бы это был указатель на свойство объект.

Возможный недостаток этого подхода состоит в том, что вы всегда получаете операцию кучи ( new и delete ), если промежуточные объекты необходимо скопировать. Это происходит, например, если вы превышаете емкость вектора и все промежуточные объекты должны быть скопированы в новый участок памяти.

В новом стандарте (т. Е.c ++ 0x) вы сможете использовать шаблон unique_ptr : его

  • можно использовать внутри стандартных контейнеров (в отличие от auto_ptr , который нельзя использовать в стандартных контейнерах),
  • предлагает обычно более быструю семантику перемещения (ее можно легко передать), а
  • заботится об удерживаемых указателях (освобождает их автоматически).
1
ответ дан 29 November 2019 в 20:16
поделиться

Способ более низкого уровня — использовать объединение

class Property
  union {
    int int_data;
    bool bool_data;
    std::cstring* string_data;
  };
  enum { INT_PROP, BOOL_PROP, STRING_PROP } data_type;
  // ... more smarts ...
};

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

РЕДАКТИРОВАТЬ: Еще немного кода, чтобы привести пример использования.

Property car = collection_of_properties.head();
if (car.data_type == Property::INT_PROP) {
  printf("The integer property is %d\n", car.int_data);
} // etc.

Вероятно, я бы поместил такую ​​логику в метод класса, где это возможно. У вас также должны быть члены, такие как этот конструктор, для синхронизации данных и поля типа:

Property::Property(bool value) {
  bool_data = value;
  data_type = BOOL_PROP;
}
10
ответ дан 29 November 2019 в 20:16
поделиться

Напишите шаблонный класс Property, производный от Property с элементом данных типа T

2
ответ дан 29 November 2019 в 20:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: