Как хранить разные типы данных в одном списке?(C ++) - PullRequest
20 голосов
/ 24 августа 2010

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

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

Ответы [ 7 ]

26 голосов
/ 25 августа 2010

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

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

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

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

9 голосов
/ 24 августа 2010

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

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;
}
4 голосов
/ 24 августа 2010

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

2 голосов
/ 24 августа 2010

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

1 голос
/ 25 августа 2010

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

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

1 голос
/ 25 августа 2010

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

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

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

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

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

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

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