Классы обертки для примитивных типов данных - PullRequest
6 голосов
/ 09 ноября 2011

При разработке решения иногда бывает удобно предоставить классы-обертки для примитивных типов данных. Рассмотрим класс, который представляет числовое значение, будь то double, float или int.

class Number {
private:
    double val;

public:
    Number(int n) : val(n) { }
    Number(float n) : val(n) { }
    Number(double n) : val(n) { }

    // Assume copy constructors and assignment operators exist

    Number& add(const Number& other) {
        val += other.val;
        return *this;
    }

    int to_int() const { return (int) val; }
    float to_float() const { return (float) val; }
    double to_double() const { return val; }
};

Теперь предположим, что у меня есть функция как таковая:

void advanced_increment(Number& n) {
    n.add(1);
}

И я бы использовал эту функцию как таковую:

Number n(2);
advanced_increment(n); // n = 3

Это звучит достаточно просто. Но что, если функция была такой?

void primitive_increment(int& n) {
    ++n;
}

Обратите внимание, что приращение является примером. Предполагается, что функция будет выполнять более сложные операции с примитивными типами данных, которые они также могут выполнять с Number типами без каких-либо проблем.

Как бы я использовал функцию точно так же, как раньше? Как в:

Number n(2);
primitive_increment(n);

Как я могу сделать мой Number класс совместимым с primitive_increment? Как я могу создать класс-оболочку для примитивных типов данных, которые были бы совместимы везде, где требуются эти типы данных?

Пока что я нашел только два решения. Одним из них является создание такой функции, как double& Number::get_value(), а затем использовать ее как primitive_increment(n.get_value());. Второе решение заключается в создании методов неявного преобразования, таких как Number::operator int&(); но это может привести ко многим неоднозначным вызовам и привести к путанице в коде.

Мне интересно, есть ли какое-либо другое решение для реализации этих типов оболочек и сохранения их примитивной функциональности.

Обновление:

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

Ответы [ 7 ]

1 голос
/ 09 ноября 2011
class Number {
    enum ValType {DoubleType, IntType} CurType;
    union {
        double DoubleVal;
        int IntVal;
    };
public:
    Number(int n) : IntVal(n), CurType(int) { }
    Number(float n) : DoubleVal(n), CurType(DoubleType) { }
    Number(double n) : DoubleVal(n), CurType(DoubleType) { }

   // Assume copy constructors and assignment operators exist

    Number& add(const Number& other) {
        switch(CurType) {
        case DoubleType: DoubleVal += other.to_double(); break;
        case IntType: IntVal+= other.to_int(); break;
        }
        return *this;
    }

    int& to_int() { 
        switch(CurType) {
        case DoubleType: IntVal = DoubleVal; CurType = IntType; break;
        //case IntType: DoubleVal = IntVal; CurType = DoubleType; break;
        }
        return IntVal; 
    }
    const int to_int() const { 
        switch(CurType) {
        case DoubleType: return (int)DoubleVal;
        case IntType: return (int)IntVal;
        }
    }
    const float to_float() const { 
        switch(CurType) {
        case DoubleType: return (float)DoubleVal;
        case IntType: return (float)IntVal;
        }
    }

    double& to_double() { 
        switch(CurType) {
        //case DoubleType: IntVal = DoubleVal; CurType = IntType; break;
        case IntType: DoubleVal = IntVal; CurType = DoubleType; break;
        }
        return DoubleVal; 
    }
    const double to_double() const { 
        switch(CurType) {
        case DoubleType: return (double)DoubleVal;
        case IntType: return (double)IntVal;
        }
    }
};

void primitive_increment(int& n) {
    ++n;
}

int main() {
    Number pi(3.1415);
    primitive_increment(pi.to_int());
    //pi now is 4
    return 0;
}

Я признаю, что это довольно неловко и не идеальная ситуация, но она решает данную проблему.

1 голос
/ 09 ноября 2011

C ++ 11 имеет явные перегрузки операторов.

struct silly_wrapper {
  int foo;
  explicit operator int&() { return foo; }
};

void primitive_increment(int& x) { ++x; }


int main()
{
   silly_wrapper x;
   primitive_increment(x); // works
   x += 1; // doesn't work - can't implicitly cast
}
0 голосов
/ 09 ноября 2011

(Это немного в темноте, поскольку я не совсем уверен, как ваш общий дизайн совмещается.)

Как насчет шаблонных бесплатных функций:

class IncTagIntegral{};
class IncTagNonintegral{};
template <bool> struct IncTag { typedef IncTagNonintegral type; }
template <> struct IncTag<true> { typedef IncTagIntegral type; }

template <typename T> void inc_impl(T & x, IncTagIntegral)
{
  ++x;
}

template <typename T> void inc_impl(T & x, IncTagNonintegral)
{
  x += T(1);
}


template <typename T> void primitive_increment(T & x)
{
  inc_impl<T>(x, typename IncTag<std::is_integral<T>::value>::type());
}

template <> void primitive_increment(Number & x)
{
  // whatever
}

Этот подход может быть распространен на другие функции, которые необходимо применять как к существующим типам, так и к вашим собственным типам.


Вот еще один длинный вариант, на этот раз с использованием стирания типов:

struct TEBase
{
   virtual void inc() = 0;
}

struct any
{
  template <typename T> any(const T &);
  void inc() { impl->inc(); }
private:
  TEBase * impl;
};

template <typename T> struct TEImpl : public TEBase
{
  virtual void inc() { /* implement */ }
  // ...
}; // and provide specializations!

template <typename T> any::any<T>(const T & t) : impl(new TEImpl<T>(t)) { }

Ключ в том, что вы предоставляете различные конкретные реализации TEImpl<T>::inc() посредством специализации, но вы можете использовать a.inc() для любого объекта a типа any.По этой идее вы можете создать дополнительные обертки со свободными функциями, например void inc(any & a) { a.inc(); }.

0 голосов
/ 09 ноября 2011

Вот еще более причудливый ответ, о котором я только что подумал:

class Number; 
template<class par, class base>
class NumberProxy {
    base Val;
    par* parent;
    NumberProxy(par* p, base v) :parent(p), Val(v) {}
    NumberProxy(const NumberProxy& rhs) :parent(rhs.parent), Val(rhs.Val) {}
    ~NumberProxy() { *parent = Val; }
    NumberProxy& operator=(const NumberProxy& rhs) {Val = rhs.Val; return *this}
    operator base& {return Val;}
};

class Number {
private:
    double val;
public:
    Number(int n) : val(n) { }
    Number(float n) : val(n) { }
    Number(double n) : val(n) { }
    // Assume copy constructors and assignment operators exist        
    int to_int() const { return (int) val; }
    float to_float() const { return (float) val; }
    double to_double() const { return val; }

    NumberProxy<Number,int> to_int() { return NumberProxy<Number,int>(this,val); }
    NumberProxy<Number,float> to_float() { return NumberProxy<Number,float>(this,val); }
    NumberProxy<Number,double> to_double() { return NumberProxy<Number,double>(this,val); }
};

void primitive_increment(int& n) {
    ++n;
}

int main() {
    Number pi(3.1415);
    primitive_increment(pi.to_int());
    //pi now is 4
    return 0;
}

Number.to_int() возвращает NumberProxy<int>, что является простотой, конвертируемой в int&, над которым работает функция.Когда функция и выражение завершены, временный NumberProxy<int> уничтожается, и его деструктор обновляет свой родительский Number обновленным значением.Это имеет дополнительное удобство: требуется лишь незначительное изменение класса Number.

Очевидно, здесь есть некоторая опасность: если вы дважды вызовете to_N() в одном и том же выражении, два int & не будут синхронизированы, илиесли кто-то берет int и после конца утверждения.

0 голосов
/ 09 ноября 2011

Сделайте преобразование частным, и функция друга сделает преобразование внутри него.

class silly_wrapper {
private:
  int foo;
  float bar;
  operator int&() { return foo; }

  template <typename T>
  friend void primitive_increment(T& x) { ++static_cast<int&>(x); }
};


int main()
{
   silly_wrapper x;
   primitive_increment(x); // works

   int i;
   primitive_increment(i); // works

   int& r = static_cast<int&>(x); // can't convert - operator is private
}
0 голосов
/ 09 ноября 2011

Если ваш класс Number не реализует подмножество int, вы просто не сможете этого сделать.Это даст неверные результаты, если, например, ваш класс Number содержит значение INT_MAX и может также содержать значение INT_MAX+1.Если ваш класс Number моделирует подмножество int, тогда возможно преобразование в int и обратно.

Кроме этого, ваш единственный шанс - переписать функцию для принятия Number объекты.В идеале сделайте его шаблоном, чтобы он мог работать как с int, так и с Number (а также с любым другим текущим или будущим классом, который представляет int -подобный интерфейс).

0 голосов
/ 09 ноября 2011

Вместо предоставления primitive_increment.Вы должны перегрузить оператор ++ для вашего Number класса и увеличить его таким образом.

Number& operator++() { ++val; return *this;}
Number& operator+=(const Number& rhs) { val += rhs.Val; return *this;}
Number operator+(const Number& rhs) { Number t(*this); t+=rhs; return t;}

см .: Операторы в C и C ++

...