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

С++ std:: map, содержащий любой тип значения

В принципе, я хочу, чтобы MyClass содержал Hashmap, который сопоставляет имя поля (string) с любым типом Значение.. Для этого я написал отдельный класс MyField, который содержит информацию о типе и значении.

Это то, что у меня есть до сих пор:

template <typename T>
class MyField {
    T m_Value;
    int m_Size;
}


struct MyClass {
    std::map<string, MyField> fields;   //ERROR!!!
}

Но, как вы можете видеть, объявление карты терпит неудачу, потому что я не предоставил параметр типа для MyField...

Итак, я думаю, что это должно быть что-то вроде

std::map< string, MyField<int> > fields;

или

std::map< string, MyField<double> > fields;


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

Есть ли способ достичь этого.?

4b9b3361

Ответ 1

Слепой ответ очень хорош (+1), но просто для завершения ответа: есть другой способ сделать это без библиотеки, используя динамическое наследование:

class MyFieldInterface
{
    int m_Size; // of course use appropriate access level in the real code...
    ~MyFieldInterface() = default;
}

template <typename T>
class MyField : public MyFieldInterface {
    T m_Value; 
}


struct MyClass {
    std::map<string, MyFieldInterface* > fields;  
}

Плюсы:

  • он знаком с любым С++-кодером
  • это не заставляет вас использовать Boost (в некоторых контекстах вам не разрешено);

Минусы:

  • вам нужно выделить объекты в хранилище кучи/свободного и использовать ссылочную семантику вместо семантики значения, чтобы манипулировать ими;
  • публичное наследование, открытое таким образом, может привести к чрезмерному использованию динамического наследования и много долгосрочных проблем, связанных с вашими типами, которые действительно слишком взаимозависимы;
  • вектор указателей является проблематичным, если он должен владеть объектами, поскольку вам нужно управлять уничтожением;

Поэтому используйте boost:: any или boost:: variant по умолчанию, если вы можете, и рассмотрите эту опцию только в противном случае.

Чтобы исправить эту последнюю точку минус, вы можете использовать интеллектуальные указатели:

struct MyClass {
    std::map<string, std::unique_ptr<MyFieldInterface> > fields;  // or shared_ptr<> if you are sharing ownership
}

Однако есть еще более проблематичный момент:

Это заставляет вас создавать объекты с помощью new/delete (или make_unique/shared). Это означает, что фактические объекты создаются в свободном хранилище (куче) в любом месте, предоставленном распределителем (в основном по умолчанию). Поэтому, хотя список объектов очень часто происходит не так быстро, как может быть из-за промахов кеша.

diagram of vector of polymorphic objects

Если вы очень часто выполняете цикл записи через этот список (игнорируйте следующее, если нет), то вам лучше использовать либо boost:: variant (если вы уже знать все конкретные типы, которые вы будете использовать) ИЛИ использовать какой-то полиморфный контейнер с типом.

diagram of polymorphic container

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

Вот пример (изображения оттуда): http://bannalia.blogspot.fr/2014/05/fast-polymorphic-collections.html

Однако этот метод потеряет интерес, если вам нужно сохранить порядок, в который вставляются объекты.

В любом случае, есть несколько возможных решений, которые во многом зависят от ваших потребностей. Если у вас недостаточно опыта в вашем случае, я предлагаю использовать либо простое решение, которое я впервые объяснил в моем примере, либо boost:: any/variant.


В качестве дополнения к этому вопросу я хочу указать очень хорошие статьи в блогах, в которых суммируются все методы стирания типа С++, которые вы могли бы использовать, с комментариями и плюсами/минусами:

Ответ 2

Используйте boost::variant (если вы знаете типы, которые вы можете сохранить, они предоставляют поддержку времени компиляции) или boost::any (для действительно любого типа - но вряд ли это возможно).

http://www.boost.org/doc/libs/1_55_0/doc/html/variant/misc.html#variant.versus-any

Изменить: я не могу подчеркнуть достаточно, что, несмотря на то, что ваше собственное решение может показаться классным, использование полной, правильной реализации в долгосрочной перспективе сэкономит вам много головной боли. boost::any реализует конструкторы копирования RHS (С++ 11), как безопасные (typeid()), так и небезопасные (немые роли) значения, с const corectness, операндами RHS и обоими типами указателей и значений.

Это правда в целом, но тем более для низкоуровневых базовых типов вы создаете все свое приложение.

Ответ 3

class AnyBase
{
public:
    virtual ~AnyBase() = 0;
};
inline AnyBase::~AnyBase() {}

template<class T>
class Any : public AnyBase
{
public:
    typedef T Type;
    explicit Any(const Type& data) : data(data) {}
    Any() {}
    Type data;
};

std::map<std::string, std::unique_ptr<AnyBase>> anymap;
anymap["number"].reset(new Any<int>(5));
anymap["text"].reset(new Any<std::string>("5"));

// throws std::bad_cast if not really Any<int>
int value = dynamic_cast<Any<int>&>(*anymap["number"]).data;

Ответ 4

С++ 17 имеет тип std::variant, который имеет возможности для хранения разных типов намного лучше, чем объединение.

Для тех, кто не на С++ 17, boost::variant реализует этот же механизм.

Для тех, кто не использует boost, https://github.com/mapbox/variant реализует гораздо более легкую версию variant для С++ 11 и С++ 14, который выглядит очень многообещающим, хорошо документированным, легким и имеет множество примеров использования.

Ответ 5

Вы также можете использовать void * и вернуть значение в правильный тип с помощью reinterpret_cast. Его метод часто используется в C в обратных вызовах.

#include <iostream>
#include <unordered_map>
#include <string>
#include <cstdint> // Needed for intptr_t
using namespace std;


enum TypeID {
    TYPE_INT,
    TYPE_CHAR_PTR,
    TYPE_MYFIELD
};    

struct MyField {
    int typeId;
    void * data;
};

int main() {

    std::unordered_map<std::string, MyField> map;

    MyField anInt = {TYPE_INT, reinterpret_cast<void*>(42) };

    char cstr[] = "Jolly good";
    MyField aCString = { TYPE_CHAR_PTR, cstr };

    MyField aStruct  = { TYPE_MYFIELD, &anInt };

    map.emplace( "Int", anInt );
    map.emplace( "C String", aCString );
    map.emplace( "MyField" , aStruct  );  

    int         intval   = static_cast<int>(reinterpret_cast<intptr_t>(map["Int"].data)); 
    const char *cstr2    = reinterpret_cast<const char *>( map["C String"].data );
    MyField*    myStruct = reinterpret_cast<MyField*>( map["MyField"].data );

    cout << intval << '\n'
         << cstr << '\n'
         << myStruct->typeId << ": " << static_cast<int>(reinterpret_cast<intptr_t>(myStruct->data)) << endl;
}