Wrapper Classes for Primitive Data Types
Asked Answered
V

7

6

In designing a solution, sometimes it may be convenient to provide wrapper classes for primitive data types. Consider a class that represents a numeric value, be it a double, a float, or 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

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

Now suppose that I have a function as such:

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

And I would use this function as such:

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

This sounds easy enough. But what if the function was like this?

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

Note that the increment is an example. It is assumed that the function would perform more complicated operations on primitive data types that they should also be able to perform on Number types without any issues.

How would I use the function exactly as before? As in:

Number n(2);
primitive_increment(n);

How could I make my Number class compatible with primitive_increment? How could I create a wrapper class for primitive data types that would be compatible anywhere that these data types are required?

So far, I have only found two solution. One is to create a function such as double& Number::get_value() and then use it like primitive_increment(n.get_value());. The second solution is to create implicit conversion methods such as Number::operator int&(); but these can result in many ambiguous calls and would make the code confusing.

I'm wondering if there is any other solution to implement these types of wrappers and retain their primitive functionality.

Update:

To further clarify, in the actual project, the intent here is to make all data types derived from one base class that is commonly referred to as Object when designing such solution. A constraint is that no outside library should be used. Therefore, if I have a container that has pointers to the type Object, it should be able to hold any arbitrary value, primitive or not, and perform any primitive operation that is allowed on Object. I hope this explains it better.

Viborg answered 9/11, 2011 at 0:7 Comment(15)
What's wrong with using primitive data types directly?Bonbon
@Pubby, Nothing, but the design dictates this approach for greater functionality and encapsulating primitive data types under complex classes.Viborg
Take a look at Boost Operators. That comes handy in cases like this.Stair
What would you actually want to happen to the value of your Number class when passed into primitive_increment? Specifically, what happens when it currently has a non-integer value (e.g. 2.5)? Or a value beyond the range of ints? Until you've decided the semantics, it's not possible to provide a proper answer to this question.Urania
It should also be pointed out that you couldn't pass a primitive float to your primitive_increment function, so it's entirely unclear what behaviour you seek to emulate here!Urania
Your update doesn't clarify things. You're suggesting that Number should inherit from Object; for what purpose? Why not just wrap each primitive type in its own class, e.g. Float, Double, Int?Urania
@OliCharlesworth, That is the ultimate intent. Float, Double, and Int would be derived from Number which would be derived from Object. I tried to simply the example in question, but I don't think it worked. :(Viborg
@teedayf: I think you need a better example, then! Everyone's getting hung up on the issue of handling conversions between primitive types. Are you really just after (for example) an Int class that can be used anywhere an int can?Urania
@OliCharlesworth, Pretty much, yes. The Int class would offer additional functionality and allow the use of completely polymorphic data structures that can contain any type of data, primitive or not.Viborg
Somehow it just sounds like some form of type erasure now. Let's call the library "reliably oriented object supertype". You have a container of objects of type, say, roost::any. The for any element x you want to say ++x. This would result in a virtual function call to the implementation's operator++, which in turn would call a type-specific operation. It's just that the idea of wrapping primitive numeric types into such thing sounds so barbaric, but for more heavy-weight objects, that might be an idea. (For a bounded set of types, perhaps we could also make roost::variable_type.)Sara
@KerrekSB, That is certainly interesting. Do you happen to have any sources that would explain how this roost::any would be implemented? I took a look at boost:any and I couldn't find anything that would point me in the right direction.Viborg
@teedayf: You just add some virtual functions to the type-erasing class and provide an abstract implementation in your implementation base class. I'll add an example to my answer. Don't use it for primitive types, though! :-SSara
@KerrekSB, Primitive data types are the only culprit in this whole thing. :P All other classes in the project are derived from a super class.Viborg
@teedayf: I have a very strong suspicion that you'd be much better off providing separate specializations or overloads for the primitives rather than attempting to shoehorn them into some virtual polymorphic hierarchy. Someone thought that was a good idea in 1995, and all we got out of that was Java.Sara
@KerrekSB, Haha. So I guess the answer is that C++ was not made for such a thing. Well I think I've got all the answers I possibly can for this question. Sorry about the original confusion. I'd be happy if someone else voted to close this as well.Viborg
B
1

C++11 has explicit operator overloads.

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
}
Bonbon answered 9/11, 2011 at 0:49 Comment(3)
More complicated than it looks since he wants foo and bar to be equal.Ubald
@MooingDuck Complicated? I'll remove bar - I'm showing how to make explicit cast overloads, not how to create unions.Bonbon
oh, I missed the word explicit. That changes things. If that opening text was there too, I missed that as well.Ubald
U
1
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;
}

I will admit this is quite awkward, and not the ideal situation, but it solves the given problem.

Ubald answered 9/11, 2011 at 0:30 Comment(1)
Very. I think the real answer is to template the function.Ubald
B
1

C++11 has explicit operator overloads.

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
}
Bonbon answered 9/11, 2011 at 0:49 Comment(3)
More complicated than it looks since he wants foo and bar to be equal.Ubald
@MooingDuck Complicated? I'll remove bar - I'm showing how to make explicit cast overloads, not how to create unions.Bonbon
oh, I missed the word explicit. That changes things. If that opening text was there too, I missed that as well.Ubald
P
0

Instead of providing it to primitive_increment. You should overload the ++ operator for your Number class and increment it that way.

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

see: Operators in C and C++

Perisarc answered 9/11, 2011 at 0:11 Comment(5)
The increment function is simply an example though. It is assumed that other complicated functions would be performed by those functions.Viborg
Indeed. Although the convention is to define operator+ in terms of operator+=.Urania
@teedayf: I think the OP wanted to know if it was possible to overload operators.Ubald
@MooingDuck, I'm perfectly aware that I could overload all valid operators for the wrapper class (I actually would be doing that regardless). But that doesn't address the question, because I still wouldn't be able to directly pass the Number object to primitive_increment seamlessly.Viborg
@teedayf: Didn't realize the intent was to pass it to a function taking an intUbald
S
0

If your Number class does not implement a subset of int, you just cannot do that. It would give wrong results if e.g. your Number class contains the value INT_MAX and can hold the value INT_MAX+1 as well. If your Number class models a subset of int, then conversion to int and back is of course an option.

Other than that, your only chance is to rewrite the function to accept Number objects. Ideally make it a template, so that it can work both with int and with Number (as well as with any other current or future class which presents an int-like interface).

Sheepshank answered 9/11, 2011 at 0:31 Comment(0)
B
0

Make the conversion operator private and have a friend function do the conversion inside of it.

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
}
Bonbon answered 9/11, 2011 at 0:40 Comment(1)
The point is that the wrapper class needs to be passed to an arbitrary set of functions.Urania
U
0

Here's an even more bizzare answer I just thought of:

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() returns a NumberProxy<int>, which is implicity convertable to an int&, which the function operates on. When the function and expression complete, the temporary NumberProxy<int> is destroyed, and it's destructor updates it's parent Number with the updated value. This has the added convenience of only requiring minor modification to the Number class.

Obviously theres's some danger here, if you call to_N() twice in the same statement, the two int&'s wont be in sync, or if someone takes a int& past the end of the statement.

Ubald answered 9/11, 2011 at 0:44 Comment(2)
That too would have dangerous semantics. Consider Number x(3); int &r = x.to_int(); r = 4;.Urania
I forgot to mention that in the answer. Edited.Ubald
S
0

(This is a bit of a shot in the dark, as I'm not entirely sure how your overall design fits together.)

How about templated free functions:

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
}

This approach may be generalizable to other functions that you need to apply both to existing types and to your own types.


Here's another long shot, this time using type erasure:

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

The key is that you provide different concrete implementations of TEImpl<T>::inc() by means of specialization, but you can use a.inc() for any object a of type any. You can build additional free-function wrappers on this idea, like void inc(any & a) { a.inc(); }.

Sara answered 9/11, 2011 at 0:45 Comment(5)
His goal seems to be to pass his Number class to arbitrary functions expecting an intUbald
@MooingDuck: Quite possibly. I can't say I'm entirely on board with this project. The OP is welcome to elaborate and perhaps provide a more illustrative example.Sara
@KerrekSB, In the actual project, the intent here is to make all data types derived from one base class that is commonly referred to as Object when designing such solution. A constraint is that no outside library should be used. Therefore, if I have a container that has pointers to the type Object, it should be able to hold any arbitrary value, primitive or not, and perform any primitive operation that is allowed on Object. I hope this explains it better.Viborg
@teedayf: Hmm... that sort of explains one aspect of what you're trying to do (though not why that would be a good idea), but not how that connects to operators and functions. Do you somehow want all functions to work magically on everything derived from your super object?Sara
@KerrekSB: Not magically, but to an extent, yes. I want this to be invisible to the user.Viborg

© 2022 - 2024 — McMap. All rights reserved.