einpoklum
einpoklum

Reputation: 131976

How should I box integer types in C++?

Suppose my code has

using foo = int32_t;

somewhere, and

using bar = int32_t;

then, for some reason, I need to distinguish between this type and other int32_ts (and their aliases). But - I still want them to behave just like int32_ts.

Now, I can write:

struct foo { int32_t value; }
struct bar { int32_t value; }

which distinguishes between the types. But - these structs don't behave like ints at all; I can't even compare foos to each others! (Well, not before C++20 anyway)

Since int32_t is not a class, I can't do:

struct foo : public int32_t { }

although that would give me exactly what I need.

So it seems what I want to achieve is a "boxing" (a-la Java, C# etc.) of plain integers into classes, and the rest will be taken care of by inheritance.

It's certainly possible to blurt out a lot of boilerplate and implement all of the relevant operators for integers: assignment, comparison, arithmetic, etc. But, you know, DRY!

If I could override operator dot, that could save me all the code, but that proposal is stuck and doesn't look like it'll go somewhere soon.

So is there something else I could leverage to avoid all of that boilerplate?

Upvotes: 0

Views: 605

Answers (2)

einpoklum
einpoklum

Reputation: 131976

Strong typedefs

Several on-and-off-site comments (including @HenriMenke) have brought up the term "strong typedef"s. C++ typedefs are "weak" - they define indistinguishable aliases. A strong typedef of a new type T as type U would make T behave like U, while not having type U.

What you want to do is define two "strong typedefs" of foo and of bar as int's.

There are at least two common strong-typedef, uh, libraries I guess you could say:

Upvotes: 0

Galik
Galik

Reputation: 48635

There is one way I have tried (but not heavily tested) to avoid repeating the boilerplate. It uses templates to easily make a new type simply by supplying a different number as a template parameter. The resulting type can be type aliased to get rid of the ugly template definition:

namespace alt {

template<std::size_t TypeId, typename Number>
class typed_number
{
public:
    explicit typed_number(Number n): n(n) {}
    typed_number(typed_number const& tn): n(tn.n) {}

    typed_number& operator= (typed_number const& tn) { this->n  = tn.n; return *this; }
    typed_number& operator+=(typed_number const& tn) { this->n += tn.n; return *this; }
    typed_number& operator-=(typed_number const& tn) { this->n -= tn.n; return *this; }
    typed_number& operator*=(typed_number const& tn) { this->n *= tn.n; return *this; }
    typed_number& operator/=(typed_number const& tn) { this->n /= tn.n; return *this; }

    explicit operator Number() const { return n; }

    bool operator==(typed_number tn) const { return this->n == tn; }
    bool operator!=(typed_number tn) const { return this->n != tn; }
    bool operator<=(typed_number tn) const { return this->n <= tn; }
    bool operator>=(typed_number tn) const { return this->n >= tn; }
    bool operator< (typed_number tn) const { return this->n <  tn; }
    bool operator> (typed_number tn) const { return this->n >  tn; }

    typed_number operator+(typed_number const& tn) const { return typed_number(this->n + tn.n); }
    typed_number operator-(typed_number const& tn) const { return typed_number(this->n - tn.n); }
    typed_number operator*(typed_number const& tn) const { return typed_number(this->n * tn.n); }
    typed_number operator/(typed_number const& tn) const { return typed_number(this->n / tn.n); }

    friend std::ostream& operator<<(std::ostream& os, typed_number<TypeId, Number> n)
        { return os << n.n; }

    friend std::istream& operator>>(std::istream& is, typed_number<TypeId, Number>& n)
        { return is >> n.n; }

private:
    Number n;
};

}  // namespace alt

// give each incompatible type a different index
using dollars = alt::typed_number<0, int>;
using cents = alt::typed_number<1, int>;

int main()
{
    auto d1 = dollars(5);
    auto d2 = dollars(9);

    auto d3 = d1 + d2;

    std::cout << d1 << " + " << d2 << " = " << d3 << '\n';
}

You create the boilerplate once as a template class and instantiate it as different types merely by supplying a unique index as the first template parameter.

Upvotes: 2

Related Questions