Подтвердить что ты не робот

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

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

Я знаю, что могу создать класс "Свойство" и расширить его с помощью разных 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.

4b9b3361

Ответ 1

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

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<Property> вместо Property*?
Рассмотрим этот код:

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 пытается очистить, удалив все свойства в векторе, перед тем, как он выходит из области действия, и принимает все указатели с ним.
Теперь, хотя это может показаться хорошим для новичков, если вы только слегка опытный разработчик С++, этот код должен поднять сигнальные сигналы, как только вы его посмотрите.

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

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

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

Ответ 2

Более низкий способ - использовать объединение

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

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

EDIT: еще один код, чтобы привести пример использования.

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

Ответ 4

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

Ответ 5

Другим возможным решением является запись промежуточного класса, управляющего указателями на классы 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 будут уничтожены, автоматически освобождая указатели, которые они удерживают. Из-за перегруженного оператора разыменования и косвенности класс Bla ведет себя как обычный указатель. В последней строке *i возвращается ссылка на объект Bla, а использование -> означает то же самое, что и указатель на объект Property.

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

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

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

Ответ 6

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

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

Ответ 7

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

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