Хранение списка произвольных объектов в C ++ - PullRequest
7 голосов
/ 03 марта 2009

В Java вы можете иметь список объектов. Вы можете добавлять объекты нескольких типов, затем извлекать их, проверять их тип и выполнять соответствующие действия для этого типа.
Например: (извиняюсь, если код не совсем правильный, я ухожу из памяти)

List<Object> list = new LinkedList<Object>();

list.add("Hello World!");
list.add(7);
list.add(true);

for (object o : list)
{
    if (o instanceof int)
        ; // Do stuff if it's an int
    else if (o instanceof String)
        ; // Do stuff if it's a string
    else if (o instanceof boolean)
        ; // Do stuff if it's a boolean
}

Какой лучший способ воспроизвести это поведение в C ++?

Ответы [ 13 ]

20 голосов
/ 03 марта 2009

boost::variant аналогично предложению Dirkgently о boost::any, но поддерживает шаблон Visitor, то есть проще добавить код для конкретного типа позже. Кроме того, он распределяет значения в стеке, а не использует динамическое распределение, что приводит к немного более эффективному коду.

РЕДАКТИРОВАТЬ: Как указывает значок в комментариях, использование variant вместо any означает, что вы можете хранить значения только из одного заранее заданного списка типов. Это часто является сильной стороной, хотя это может быть слабостью в случае спрашивающего.

Вот пример (хотя и не используется шаблон Visitor):

#include <vector>
#include <string>
#include <boost/variant.hpp>

using namespace std;
using namespace boost;

...

vector<variant<int, string, bool> > v;

for (int i = 0; i < v.size(); ++i) {
    if (int* pi = get<int>(v[i])) {
        // Do stuff with *pi
    } else if (string* si = get<string>(v[i])) {
        // Do stuff with *si
    } else if (bool* bi = get<bool>(v[i])) {
        // Do stuff with *bi
    }
}

(И да, технически вы должны использовать vector<T>::size_type вместо int для типа i, и вы все равно должны технически использовать vector<T>::iterator вместо этого, но я пытаюсь сделать его простым.)

14 голосов
/ 03 марта 2009

Ваш пример с использованием Boost.Variant и посетителя:

#include <string>
#include <list>
#include <boost/variant.hpp>
#include <boost/foreach.hpp>

using namespace std;
using namespace boost;

typedef variant<string, int, bool> object;

struct vis : public static_visitor<>
{
    void operator() (string s) const { /* do string stuff */ }
    void operator() (int i) const { /* do int stuff */ }
    void operator() (bool b) const { /* do bool stuff */ }      
};

int main() 
{
    list<object> List;

    List.push_back("Hello World!");
    List.push_back(7);
    List.push_back(true);

    BOOST_FOREACH (object& o, List) {
        apply_visitor(vis(), o);
    }

    return 0;
}

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

13 голосов
/ 03 марта 2009

C ++ не поддерживает гетерогенные контейнеры.

Если вы не собираетесь использовать boost, взломать создание фиктивного класса и получить все разные классы из этого фиктивного класса. Создайте контейнер по вашему выбору для хранения фиктивных объектов класса, и вы готовы к работе.

class Dummy {
   virtual void whoami() = 0;
};

class Lizard : public Dummy {
   virtual void whoami() { std::cout << "I'm a lizard!\n"; }
};


class Transporter : public Dummy {
   virtual void whoami() { std::cout << "I'm Jason Statham!\n"; }
};

int main() {
   std::list<Dummy*> hateList;
   hateList.insert(new Transporter());
   hateList.insert(new Lizard());

   std::for_each(hateList.begin(), hateList.end(), 
                 std::mem_fun(&Dummy::whoami));
   // yes, I'm leaking memory, but that's besides the point
}

Если вы собираетесь использовать boost, вы можете попробовать boost::any. Здесь - пример использования boost::any.

Вы можете найти эту превосходную статью от двух ведущих специалистов по C ++.

Теперь, boost::variant - это еще одна вещь, на которую стоит обратить внимание, как упоминалось j_random_hacker Итак, вот сравнение , чтобы получить четкое представление о том, что использовать.

С boost::variant приведенный выше код будет выглядеть примерно так:

class Lizard {
   void whoami() { std::cout << "I'm a lizard!\n"; }
};

class Transporter {
   void whoami() { std::cout << "I'm Jason Statham!\n"; }
};

int main() {

   std::vector< boost::variant<Lizard, Transporter> > hateList;

   hateList.push_back(Lizard());
   hateList.push_back(Transporter());

   std::for_each(hateList.begin(), hateList.end(), std::mem_fun(&Dummy::whoami));
}
12 голосов
/ 03 марта 2009

Как часто такие вещи действительно полезны? Я программировал на C ++ довольно много лет, в разных проектах, и никогда не хотел гетерогенного контейнера. По какой-то причине это может быть распространенным явлением в Java (у меня гораздо меньше опыта работы с Java), но для любого конкретного использования его в проекте Java может быть способ сделать что-то другое, что будет лучше работать в C ++.

C ++ уделяет больше внимания безопасности типов, чем Java, и это очень небезопасно для типов.

Тем не менее, если объекты не имеют ничего общего, почему вы храните их вместе?

Если у них есть что-то общее, вы можете создать класс, от которого они будут наследовать; поочередно используйте boost :: any. Если они наследуют, имеют виртуальные функции для вызова или используют dynamic_cast <>, если вам действительно нужно.

3 голосов
/ 05 марта 2009

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

class MyData
{
public:
  // base classes of polymorphic types should have a virtual destructor
  virtual ~MyData() {} 

  // hand off to protected implementation in derived classes
  void DoSomething() { this->OnDoSomething(); } 

protected:
  // abstract, force implementation in derived classes
  virtual void OnDoSomething() = 0;
};

class MyIntData : public MyData
{
protected:
  // do something to int data
  virtual void OnDoSomething() { ... } 
private:
  int data;
};

class MyComplexData : public MyData
{
protected:
  // do something to Complex data
  virtual void OnDoSomething() { ... }
private:
  Complex data;
};

void main()
{
  // alloc data objects
  MyData* myData[ 2 ] =
  {
    new MyIntData()
  , new MyComplexData()
  };

  // process data objects
  for ( int i = 0; i < 2; ++i ) // for each data object
  {
     myData[ i ]->DoSomething(); // no type cast needed
  }

  // delete data objects
  delete myData[0];
  delete myData[1];
};
2 голосов
/ 04 марта 2009

Хотя вы не можете хранить примитивные типы в контейнерах, вы можете создавать классы-оболочки примитивных типов, которые будут похожи на примитивные типы Java с автоматической коробкой (в вашем примере примитивные типизированные литералы фактически автоматически упаковываются); экземпляры которых появляются в коде C ++ (и могут (почти) использоваться) точно так же, как примитивные переменные / члены данных.

См. Оболочки объектов для встроенных типов из Структуры данных и алгоритмы с объектно-ориентированными шаблонами проектирования в C ++ .

С обернутым объектом вы можете использовать оператор c ++ typeid () для сравнения типа. Я почти уверен, что сработает следующее сравнение: if (typeid(o) == typeid(Int)) [где Int будет обернутым классом для примитивного типа int и т. Д ...] (иначе просто добавьте функцию к вашим примитивным оболочкам, которая возвращает typeid и таким образом: if (o.get_typeid() == typeid(Int)) ...

Как говорится, в отношении вашего примера это имеет кодовый запах для меня. Если только это не единственное место, где вы проверяете тип объекта, Я был бы склонен использовать полиморфизм (особенно если у вас есть другие методы / функции, специфичные для типа). В этом случае я бы использовал примитивные оболочки, добавив интерфейсный класс, объявляющий отложенный метод (для выполнения 'do stuff'), который будет реализован каждым из ваших обернутых примитивных классов. При этом вы сможете использовать контейнерный итератор и исключить оператор if (опять же, если у вас есть только одно сравнение типов, настройка отложенного метода с использованием полиморфизма только для этого будет излишним).

2 голосов
/ 03 марта 2009

Просто для полноты этой темы я хочу упомянуть, что на самом деле вы можете сделать это с чистым C, используя void *, а затем преобразовать его во что бы то ни было (хорошо, мой пример не чистый C, поскольку он использует векторы но это спасает меня некоторый код). Это будет работать, если вы знаете, какой тип у ваших объектов, или если вы храните поле где-то, которое запоминает это. Вы наверняка не хотите этого делать, но вот пример, демонстрирующий, что это возможно:

#include <iostream>
#include <vector>

using namespace std;

int main() {

  int a = 4;
  string str = "hello";

  vector<void*> list;
  list.push_back( (void*) &a );
  list.push_back( (void*) &str );

  cout <<  * (int*) list[0] << "\t" << * (string*) list[1] << endl;

  return 0;
}
2 голосов
/ 03 марта 2009

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

1 голос
/ 03 марта 2009

Кроме того, как отмечали большинство, вы не можете этого сделать, или, что более важно, более чем вероятно, вы действительно не хотите.

Давайте отклоним ваш пример и рассмотрим что-то ближе к реальному примеру. В частности, некоторый код, который я видел в реальном проекте с открытым исходным кодом. Он попытался эмулировать процессор в массиве символов. Следовательно, он помещает в массив однобайтовый «код операции», за которым следуют 0, 1 или 2 байта, которые могут быть символом, целым числом или указателем на строку, в зависимости от кода операции. Чтобы справиться с этим, потребовалось немало потрясений.

Мое простое решение: 4 отдельных стека <> s: по одному для перечисления opcode и по одному для символов, целых и строки. Снимите следующий со стека кодов операций, и вам потребуется, какой из трех других получить операнд.

Очень велика вероятность того, что ваша настоящая проблема может быть решена аналогичным образом.

1 голос
/ 03 марта 2009

Я довольно неопытный, но вот что я бы пошел ...

  1. Создайте базовый класс для всех классов, которыми вы должны манипулировать.
  2. Написать класс контейнера / повторно использовать класс контейнера. (Пересмотрен после просмотра других ответов. Мой предыдущий пункт был слишком загадочным.)
  3. Написать похожий код.

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

Надеюсь, это поможет.

...