Классы-оболочки для примитивных типов данных

#c #type-conversion #wrapper #primitive

#c #преобразование типов #оболочка #примитивный

Вопрос:

При разработке решения иногда может быть удобно предоставить классы-оболочки для примитивных типов данных. Рассмотрим класс, который представляет числовое значение, будь то a double , a float или an 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

    Numberamp; add(const Numberamp; 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(Numberamp; n) {
    n.add(1);
}
  

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

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

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

 void primitive_increment(intamp; n) {
      n;
}
  

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

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

 Number n(2);
primitive_increment(n);
  

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

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

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

Обновить:

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

Комментарии:

1. Что плохого в непосредственном использовании примитивных типов данных?

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

3. Взгляните на операторы Boost. Это удобно в подобных случаях.

4. @teedayf: Тогда, я думаю, вам нужен пример получше! Все зацикливаются на проблеме обработки преобразований между примитивными типами. Вам действительно нужен (например) Int класс, который можно использовать везде, где это int возможно?

5. @teedayf: У меня есть очень сильное подозрение, что вам было бы гораздо лучше предоставить отдельные специализации или перегрузки для примитивов, а не пытаться втиснуть их в какую-то виртуальную полиморфную иерархию. Кто-то подумал, что это хорошая идея в 1995 году, и все, что мы получили от этого, — Java.

Ответ №1:

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

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

void primitive_increment(intamp; x) {   x; }


int main()
{
   silly_wrapper x;
   primitive_increment(x); // works
   x  = 1; // doesn't work - can't implicitly cast
}
  

Комментарии:

1. Сложнее, чем кажется, поскольку он хочет, чтобы foo и bar были равны.

2. @MooingDuck Сложно? Я удалю bar — я показываю, как создавать явные перегрузки приведения, а не как создавать объединения.

3. о, я пропустил слово explicit . Это меняет дело. Если этот вступительный текст там тоже был, я его тоже пропустил.

Ответ №2:

 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

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

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

    doubleamp; 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(intamp; n) {
      n;
}

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

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

Комментарии:

1. Очень. Я думаю, что реальный ответ заключается в шаблоне функции.

Ответ №3:

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

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

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

Комментарии:

1. Однако функция увеличения — это просто пример. Предполагается, что эти функции будут выполнять другие сложные функции.

2. Действительно. Хотя соглашение заключается в том, чтобы определять operator в терминах operator = .

3. @teedayf: Я думаю, что OP хотел знать, возможно ли перегружать операторы.

4. @MooingDuck, я прекрасно понимаю, что я мог бы перегрузить все допустимые операторы для класса-оболочки (на самом деле я бы сделал это независимо). Но это не решает вопрос, потому что я все равно не смог бы напрямую передать Number объект primitive_increment без проблем.

5. @teedayf: Не понял, что целью было передать его функции, выполняющей int

Ответ №4:

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

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

Ответ №5:

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

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

  template <typename T>
  friend void primitive_increment(Tamp; x) {   static_cast<intamp;>(x); }
};


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

   int i;
   primitive_increment(i); // works

   intamp; r = static_cast<intamp;>(x); // can't convert - operator is private
}
  

Комментарии:

1. Дело в том, что класс-оболочку необходимо передать произвольному набору функций.

Ответ №6:

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

 class Number; 
template<class par, class base>
class NumberProxy {
    base Val;
    par* parent;
    NumberProxy(par* p, base v) :parent(p), Val(v) {}
    NumberProxy(const NumberProxyamp; rhs) :parent(rhs.parent), Val(rhs.Val) {}
    ~NumberProxy() { *parent = Val; }
    NumberProxyamp; operator=(const NumberProxyamp; rhs) {Val = rhs.Val; return *this}
    operator baseamp; {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(intamp; n) {
      n;
}

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

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

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

Комментарии:

1. Это тоже имело бы опасную семантику. Рассмотрим Number x(3); int amp;r = x.to_int(); r = 4; .

2. Я забыл упомянуть об этом в ответе. Отредактировано.

Ответ №7:

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

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

 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 amp; x, IncTagIntegral)
{
    x;
}

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


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

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

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


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

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

struct any
{
  template <typename T> any(const T amp;);
  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 amp; t) : impl(new TEImpl<T>(t)) { }
  

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

Комментарии:

1. Его цель, похоже, состоит в том, чтобы передать свой Number класс произвольным функциям, ожидающим int

2. @MooingDuck: Вполне возможно. Я не могу сказать, что я полностью согласен с этим проектом. Приветствуется разработка OP и, возможно, предоставление более наглядного примера.

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

4. @teedayf: Хм … это как бы объясняет один аспект того, что вы пытаетесь сделать (хотя и не то, почему это было бы хорошей идеей), но не то, как это связано с операторами и функциями. Вы каким-то образом хотите, чтобы все функции волшебным образом работали со всем, что получено из вашего суперобъекта?

5. @KerrekSB: Не волшебным образом, но в какой-то степени, да. Я хочу, чтобы это было невидимо для пользователя.