контейнер смешанных типов в C ++ (аналогично nsdictionary) - PullRequest
3 голосов
/ 21 августа 2010

Что я хотел бы сделать (в C ++), это создать тип данных «Параметр», который имеет значение, min и max.Затем я хотел бы создать контейнер для этих типов.

Например, у меня есть следующий код:

    template <typename T>
    class ParamT {
    public:
        ParamT() {
        }

        ParamT(T _value):value(_value) {
        }

        ParamT(T _value, T _vmin, T _vmax):value(_value), vmin(_vmin), vmax(_vmax) {
        }


        void setup(T vmin, T vmax) {
            this->vmin      = vmin;
            this->vmax      = vmax;
        }

        void setup(T value, T vmin, T vmax) {
            setup(vmin, vmax);
            setValue(value);
        }

        T operator=(const T & value) {
            setValue(value);
        }



        void setValue(T v) {
            value = v;
        }

        T getValue() {
            return value;
        }

        operator T() {
            return getValue();
        }

    protected:

        T       value;
        T       vmin;
        T       vmax;
    };

    typedef ParamT<int>     Int;
    typedef ParamT<float>   Float;
    typedef ParamT<bool>    Bool;

В идеальном мире мой API будет выглядеть примерно так:

std::map<string, Param> params;
params["speed"] = PFloat(3.0f, 2.1f, 5.0f);
params["id"] = PInt(0, 1, 5);

или

params["speed"].setup(3.0f, 2.1f, 5.0f);
params["id"].setup(0, 1, 5);

и запись в них:

params["speed"] = 4.2f;
params["id"] = 1;

или

params["speed"].setValue(4.2f);
params["id].setValue(1);

и чтение:

float speed = params["speed"];
int id = params["id"];

или

float speed = params["speed"].getValue();
int id = params["id"].getValue();

Конечно, в приведенном выше коде у ParamT нет базового класса, поэтому я не могу создать карту.Но даже если я создам базовый класс для него, который расширяет ParamT, у меня, очевидно, не может быть разных getValues ​​(), которые возвращают разные типы.Я думал о многих решениях, включая setValueI (int i), setValuef (float f), int getValueI (), float getValueF (), или карту для int, карту для float и т. Д. Но все кажется очень нечистым.Возможно ли в C ++ реализовать вышеуказанный API?

В настоящее время я занимаюсь только простыми типами, такими как int, float, bool и т. Д. Но я хотел бы распространить это на векторы (мои собственные) и потенциальноБольше.

Ответы [ 5 ]

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

Это сложная концепция для реализации в C ++, как вы видите.Я всегда сторонник использования библиотеки Boost, которая уже решила ее для вас .Вы можете определить класс шаблона сложного варианта Boost для чего-то более удобного в вашем конкретном домене, поэтому

typedef boost::variant< int, float, bool > ParamT; 
class Param
{
   public:
      // initialize the variants
      Param(ParamT min, ParamT max, ParamT value)
       : m_Min(min), m_Max(max), m_Value(value) {}

      // example accessor
      template<typename OutT>
      const ParamT& value()
      {
          return boost::get<OutT>(m_Value);
      }
      // other accessors for min, max ...
   private:
      ParamT m_Min, m_Value, m_Max;
};



Param speed(-10.0f, 10.0f, 0.0f);
float speedValue = speed.value<float>();

Теперь, чтобы добавить другой тип к вашему варианту (например, long, std :: string, что угодно), выможно просто изменить typedef для ParamT;Суть в том, что бремя проверки типов ложится на вас - будет исключение, если вы храните float и пытаетесь получить int, но безопасность во время компиляции отсутствует.

Еслиесли вы хотите по-настоящему сойти с ума, вы можете реализовать перегруженный оператор приведения к прокси-объекту ....

class ProxyValue
{
public:
  ProxyValue(ParamT& value) : m_Value(value) {}
  template<typename ValueT>
  operator ValueT()
  {
     return boost::get<ValueT>(m_Value);
  }
private:
  ParamT& m_Value;
};

Вы бы вернули это из функции без шаблонов value () в Param вместосам вариант.Теперь вы можете присвоить значение без вызова шаблона ..

Param speed(-10.0f, 0, 10);
float speedValue = speed.value();

Хотя это и справедливое предупреждение, вы попадаете в ад метапрограммирования.Здесь будут драконы.И как всегда, это не полное решение, просто указатель.YMMV.

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

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

Вы можете использовать либо boost :: any (чтобы иметь возможность хранить любой тип), либо boost :: variable (чтобы сохранить любой тип из фиксированного набора предварительно определенных типов);однако библиотека boost :: program_options в основном уже делает то, что вы хотите.Я настоятельно рекомендую вам использовать boost :: program_options, а не использовать эту библиотеку самостоятельно.Я должен указать на то, что в том, что вы делаете, есть серьезная обратная сторона;вы проверяете типы вручную во время выполнения, что облегчает проскальзывание различных ошибок.Я настоятельно рекомендую использовать буферы протокола в качестве языка конфигурации, так как вы получаете более строгую проверку типов таким образом.

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

У меня вопрос по поводу вашего дизайна: зачем вам поддерживать все эти типы значений?Производительность, безопасность типов, числовая точность или простота / простота использования?Будет сложно заставить ваш интерфейс поддерживать все это.

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

Если вам нужно идеальное хранилище, вы можете реализовать свой собственный числовой тип, который можетпреобразования (неявные или явные) в различные числовые типы и поддержание идеального хранилища при преобразовании в / из одного и того же типа.Если вы действительно обеспокоены идеальным хранилищем, вы также можете его сбросить, если попытаетесь выполнить преобразование обратно в неправильный тип.Это похоже на строго типизированный союз.Я считаю, что библиотека Boost имеет такой тип. Редактировать: В ответе Николаса М. Т. Эллиотта уже упоминается это - вариант повышения.

Если вам нравится еще более явный интерфейс, который у вас здесь есть, с вашим интерфейсом GetValueAsInt / SetValueAsInt вы можетесделать это немного проще.Объедините сеттеры, поскольку C ++ поддерживает перегрузку функций для параметров: void SetValue(int value) void SetValue(float value).C ++ не поддерживает перегрузку функций для типов возвращаемых данных, поэтому вы не можете объединить получатели.

Редактировать:

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

Самый простой способ обойти это в C ++ - это использовать void* в качестве типа значения и выполнить приведение для преобразования его ви от вашего целевого типа.Ваша библиотека может предоставить оболочку для шаблона, чтобы выполнить это приведение, и бросить, если приведение не выполнено.

Это похоже на использование "объекта" в Java / C #

Редактировать:

Как предложил Майкл Аарон Сафян, вы можете использовать boost :: any.

В конце вам нужно подумать об этом: must ваш дизайн включает словари свойств?Если у него нет , то вы можете воспользоваться статическим анализом компилятора, если откажетесь от этой идеи.Любое поведение, которое вы откладываете во время выполнения, приведет к ошибкам, которых вы не найдете во время компиляции.Это ускоряет запуск кода, но усложняет обработку ошибок во время выполнения и может ухудшить производительность.

0 голосов
/ 21 августа 2010

Хорошо, мне скучно на работе (просто жду, когда что-нибудь скомпилируется), так что вот еще одно решение. Просто есть один тип Param, который хранит три Value s. Эти значения могут быть динамически напечатаны и могут хранить целые числа и числа с плавающей запятой (и все, что вам нужно).

    class Value
    {
    private:
        union 
        {
            int i,
            float f
        } val;

        DataTypeCode dtc;
    public
        Value() : val.i(0), dtc(INT) {}
        Value(int i) : val.i(i), dtc(INT) {}
        Value(float f) : val.f(f), dtc(FLOAT) {}
        Value& operator=(int i) 
        {
            val.i=i; 
            dtc=INT; 
            return *this;
        }

        Value& operator=(float f) 
        {
            val.f=f; 
            dtc=FLOAT; 
            return *this;
        }

        operator int() 
        {
            switch (dtc)
            {
                case INT: return val.i;
                case FLOAT: return (int)val.f;
            }
            return 0;
        }

        operator float()
        {
            switch (dtc)
            {
                case INT: return (float)val.i;
                case FLOAT: return val.f;
            }
            return 0;
        }
    }

    class Param
    {
    private:
        Value value, min, max 
    public:
        Param(Value value, Value min, Value max) : value(value), min(min), max(max) {}
    }

обратите внимание, для этого по-прежнему требуется перечисление DataTypeCode, которое есть в моем другом ответе.

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

    std::map<string:Param> Params;

    Params["speed"]=Param(1.4,0.1,5.6)

    float speed=Params["speed"]

операторы приведения вместе с перегруженными конструкторами и функциями operator = автоматически преобразуют типы для вас.

0 голосов
/ 21 августа 2010

Ну, легко создать контейнерный магазин практически для всего. Как вы сказали, вы можете создать общий базовый класс, и карта будет просто хранить указатель на это. Сложной частью является знание того, что это за тип данных, когда вы их извлекаете и используете. У меня есть что-то вроде этого в моем основном проекте, где я смешиваю код на языке C ++, определенный во время компиляции, и код, определенный на этапе исполнения, с другого языка. Поэтому я включил в класс его код типа данных, чтобы я мог сделать с ним оператор switch(). Вы могли бы иметь что-то вроде этого:

    enum DataTypeCode
    {
        UNKNOWN,
        INT,
        FLOAT
    };

    template <class DataType>
    DataTypeCode GetDataTypeCode()
    {
        return UNKNOWN;
    }

    template <>
    DataTypeCode GetDataTypeCode<int>()
    {
        return INT;
    }

    template <>
    DataTypeCode GetDataTypeCodE<float>(
    {
        return FLOAT;
    }

    class BaseParam
    {
    public:
        virtual ~BaseParam() {}
        virtual DataTypeCode GetDataTypeCode()=0;
    };

    template <class DataType>
    class Param : public BaseParam
    {
    public:
        DataTypeCode GetDataTypeCode()
        {
            return ::GetDataTypeCode<DataType>();
        }
    }

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

    std::map<string,BaseParam*> Params

    Params["speed"]=new Param<float>(...)

    BaseParam* pMyParam=Params["speed"];
    switch (pMyParam->GetDataTypeCode())
    {
        case INT:
            //dosomething with int types
        case FLOAT:
            //dosomething with float types
    }

Это не красиво, но это сделает работу. Обычно я заканчиваю тем, что обертываю std::map<string, BaseParam*> внутри другого класса, чтобы скрыть тот факт, что он хранит указатели. Мне нравится, когда мои API-интерфейсы максимально скрывают использование указателей, поэтому младшим программистам в моей команде легче с этим справляться.

...