2010-08-24 2 views
19

Мне нужно сохранить список различных свойств объекта. Свойство состоит из имени и данных, которые могут быть любого типа данных.Как хранить разные типы данных в одном списке? (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.

+0

Аналогично тому, как вы хотите использовать Boost.Variant: http://stackoverflow.com/questions/1358427/function-which-returns-an-unknown-type – AraK

+0

Boost - это не вариант. – hasdf

+0

Ваши классы 'PropertyXxxx' должны наследовать от' class Property'. Вы можете отредактировать свой пост. –

ответ

23

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; 

Edit: Почему с помощью 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.

+0

Но 'std :: shared_ptr' еще не является частью C++. И если ему не разрешено использовать Boost, ему, скорее всего, не разрешат использовать расширения TR1 и C++ 0x. – phlipsy

+0

@phlipsy: О, я просто упустил из виду утверждение о повышении. Это глупо. Во всяком случае, если магазин не позволяет повысить, у них должен быть свой собственный умный указатель в любом случае - [так плохо, как обычно) (http://stackoverflow.com/questions/1437053/boost-advocacy-help- необходимо/1437748 # 1437748), это все же лучше, чем отсутствие умных указателей. Кроме того, некоторые компиляторы уже поддерживают 'std :: shared_ptr'. – sbi

+0

В чем смысл shared_ptr здесь по сравнению с обычным указателем? – hasdf

9

Путь нижнего уровня является использование союзной

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

Не могли бы вы рассказать? Я не понимаю этого – hasdf

+2

@hasdf: Объединение позволяет хранить любой из составных типов, но только по одному (в основном все члены имеют один и тот же регион памяти). И тогда вам решать использовать поле 'data_type', чтобы отслеживать, какой тип фактически хранится. – casablanca

2

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

+0

Вы по-прежнему не сможете помещать их в массив или контейнер stl. Недвижимость и Недвижимость - совершенно разные типы. – Dima

+1

@Dima: Да, но в этом примере они все равно хранятся указателем. – 2010-08-24 18:04:53

+2

@ Dima: Нет, если у них есть общий суперкласс, который, как мне кажется, означает, что jdv означает «происходит от« Property ». –

0

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

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

1

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

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

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

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

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

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

Смежные вопросы