Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a convenient way to wrap std::pair as a new type?

Tags:

c++

stl

Often times I find myself using std::pair to define logical groupings of two related quantities as function arguments/return values. Some examples: row/col, tag/value, etc.

Often times I should really be rolling my own class instead of just using std::pair. It's pretty easy to see when things start breaking down - when the code becomes littered with make_pair, first, and second, its very hard to remember what is what - an std::pair<int, int> conveys less meaning than a type Position.

What have you found are the best ways to wrap the functionality of std::pair in a type that conveys real meaning?

Here are some things I have considered:

typedef std::pair<int, int> Position;

This at least gives the type a meaningful name when passing it around, but the type isn't enforced, its still really just a pair, and most of the same problems still exist. This is however very simple to write.

struct Position : public std::pair<int, int>
{
    typedef std::pair<int, int> Base;
    Position() : Base() {}
    Position(const Position &x) : Base(x) {}
    Position(int a, int b) : Base(a, b) {}

    int &row() { return first; }
    const int &row() const { return first; }

    int &col() { return second; }
    const int &col() const { return second; }
};

This is better, since we can access the variables via a reasonably descriptive name. The problem here is that you can still access first and second, so its easy for the abstraction to leak. Also, accessing simple variables via functions makes the syntax annoying.

The obvious next step is to make the inheritance private:

struct Position : private std::pair<int, int>
{
    typedef std::pair<int, int> Base;
    Position() {}
    Position(const Position &x) : Base(x) {}
    Position(int a, int b) : Base(a, b) {}

    int &row() { return first; }
    const int &row() const { return first; }

    int &col() { return second; }
    const int &col() const { return second; }

    bool operator<(const Position &x) const { return Base(*this) < Base(x); }
    // other forwarding operators as needed...
};

So now at least we have gotten rid of the access to first and second, but now a new problem pops up. When we want to store the type in an std::set, we now don't have access to the operator< overload since we don't have access to first and second. This means we have to define a forwarding function for each operator overload we want. For me this is usually ==, !=, and <, but there could be others that I'd want. Yes I know I probably shouldn't overload operator< just to stick it in an associative container, but it makes everything so darn simple... And defining these operators for each new type is a pain, and we STILL have to access via functions. We can fix that:

struct Position
{
    Position() {}
    Position(const Position &x) : row(x.row), col(x.col) {}
    Position(int row, int col) : row(row), col(col) {}

    int row, col;
};
bool operator<(const Position &a, const Position &b)
{
    return a.row < b.row || (!(b.row < a.row) && a.col < b.col);
}
// more overloads as needed

So now we have simple variable access, but now defining overloaded operators is even more of a pain, because instead of just forwarding them to the pair's implementation, we actually have to re-implement them each time...

Are there any solutions I have overlooked that make this easy without the drawbacks? If there aren't which would you tend to prefer?

like image 289
Greg Rogers Avatar asked Oct 14 '08 18:10

Greg Rogers


2 Answers

This is what Boost.Tuple was made for.

But you should probably be using std::tuple now...

like image 137
Ferruccio Avatar answered Sep 20 '22 14:09

Ferruccio


A coworker pointed me to two possible solutions:

Using boost strong typedef as an improved version of the typedef. I'd never heard of this before, and it doesn't seem to really be part of any sub-library, just kind of floating.

Using a macro to generate the code needed for the different operators. This way I wouldn't have to explicitly write anything on a per definition level, just do something like DEFINE_PAIR_TYPE(Position, int, int, row, col);. This is probably closest to what I'm looking for, but it still feels kind of evil compared to some of the solutions presented by others.

like image 35
Greg Rogers Avatar answered Sep 20 '22 14:09

Greg Rogers