Хранение списка произвольных объектов в C++

StackOverflow https://stackoverflow.com/questions/607458

  •  03-07-2019
  •  | 
  •  

Вопрос

В 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++?

Это было полезно?

Решение

Ваш пример с использованием 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, легко забыть внести изменения повсюду и создать ошибку.

Другие советы

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

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

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

#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 в любом случае вместо этого, но я стараюсь, чтобы это было проще.)

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

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

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

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

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

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

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

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

Просто для полноты этой темы я хочу упомянуть, что вы действительно можете сделать это на чистом 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;
}

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

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

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

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

Я довольно неопытен, но вот что я бы сделал:

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

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

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

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

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

Мое простое решение:4 отдельных стека<>:Один для перечисления «код операции» и по одному для символов, целых чисел и строк.Возьмите следующий код операции из стека, и вы увидите, какой из трех остальных получит операнд.

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

Ну, вы можете создать базовый класс, а затем создавать классы, которые наследуются от него.Затем сохраните их в std::vector.

Краткий ответ...ты не можешь.

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

RTTI (информация о типе времени выполнения) в C++ всегда была сложной задачей, особенно при использовании кросс-компилятора.

Лучше всего использовать STL и определить интерфейс для определения типа объекта:

public class IThing
{
   virtual bool isA(const char* typeName);
}

void myFunc()
{
   std::vector<IThing> things;

   // ...

   things.add(new FrogThing());
   things.add(new LizardThing());

   // ...

   for (int i = 0; i < things.length(); i++)
   {
       IThing* pThing = things[i];

       if (pThing->isA("lizard"))
       {
         // do this
       }
       // etc
   }
}

Майк

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top