12

As most programmers I admire and try to follow the principles of Literate programming, but in C++ I routinely find myself using std::pair, for a gazillion common tasks. But std::pair is, IMHO, a vile enemy of literate programming...

My point is when I come back to code I've written a day or two ago, and I see manipulations of a std::pair (typically as an iterator) I wonder to myself "what did iter->first and iter->second mean???".

I'm guessing others have the same doubts when looking at their std::pair code, so I was wondering, has anyone come up with some good solutions to recover literacy when using std::pair?

stefanB
  • 77,323
  • 27
  • 116
  • 141
Robert Gould
  • 68,773
  • 61
  • 187
  • 272

8 Answers8

17

std::pair is a good way to make a "local" and essentially anonymous type with essentially anonymous columns; if you're using a certain pair over so large a lexical space that you need to name the type and columns, I'd use a plain struct instead.

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • I was starting to write an answer suggesting a wrapper class to give the accessors a name, but yours is much simpler. Unless the OP needs the capabilities of an STL container. – DevSolar Jun 25 '09 at 05:16
  • This solution is quite reasonable, but DevSolar's idea would be useful when working with std::map, subclassing the iterator might be a nice solution – Robert Gould Jun 25 '09 at 05:21
  • For the kind of use cases for std::pair, I usually find the struct too much effort. The simplest struct (ie. just two members) is an Aggregate and so requires aggregate initialization or member by member initialization - both of which require a local temporary object. Alternatively you can add a constructor or write a make_pair equivalent, both of which complicate the situation IMHO. – Richard Corden Jun 25 '09 at 09:40
6

How about this:

struct MyPair : public std::pair < int, std::string >
{
    const int& keyInt() { return first; }
    void keyInt( const int& keyInt ) { first = keyInt; }
    const std::string& valueString() { return second; }
    void valueString( const std::string& valueString ) { second = valueString; }
};

It's a bit verbose, however using this in your code might make things a little easier to read, eg:

std::vector < MyPair > listPairs;

std::vector < MyPair >::iterator iterPair( listPairs.begin() );
if ( iterPair->keyInt() == 123 )
    iterPair->valueString( "hello" );

Other than this, I can't see any silver bullet that's going to make things much clearer.

Alan
  • 13,510
  • 9
  • 44
  • 50
  • well besides giving parts names like customerId() and customerName(), the solution is viable; just need to change keyInt with customerId – Robert Gould Jun 26 '09 at 07:48
  • 1
    I implemented this, using a macro to define the wrapper classes. WRAPPER(wrapperName,baseName,firstName,secondName) so I write WRAPPER(customer,_cutomerpair,id,name) so I can access my stuff as customer.id() and customer.name() also instead of a getter/setter I just use one key_type& firstName() – Robert Gould Jun 26 '09 at 08:06
4
typedef std::pair<bool, int> IsPresent_Value;
typedef std::pair<double, int> Price_Quantity;

...you get the point.

rlbond
  • 65,341
  • 56
  • 178
  • 228
  • 1
    Can you explain how this helps? Sure where you declare the pair it's obvious, but if you're in the body of a functor with nothing but "value_type" as the parameter, you have no idea what "first" and "second" are. – Richard Corden Jun 25 '09 at 09:34
  • A modern IDE should tell you a variable's type when you hover over it. Or have an option to flip to its declaration. – rlbond Jun 25 '09 at 16:04
2

You can create two pairs of getters (const and non) that will merely return a reference to first and second, but will be much more readable. For instance:

string& GetField(pair& p) { return p.first; }
int& GetValue(pair& p) { return p.second; }

Will let you get the field and value members from a given pair without having to remember which member holds what.

If you expect to use this a lot, you could also create a macro that will generate those getters for you, given the names and types: MAKE_PAIR_GETTERS(Field, string, Value, int) or so. Making the getters straightforward will probably allow the compiler to optimize them away, so they'll add no overhead at runtime; and using the macro will make it a snap to create those getters for whatever use you make of pairs.

Eran
  • 21,632
  • 6
  • 56
  • 89
2

Recently I've found myself using boost::tuple as a replacement for std::pair. You can define enumerators for each member and so it's obvious what each member is:

typedef boost::tuple<int, int> KeyValueTuple;
enum {
  KEY
  , VALUE
};

void foo (KeyValueTuple & p) {
    p.get<KEY> () = 0;
    p.get<VALUE> () = 0;
}

void bar (int key, int value)
{
  foo (boost:tie (key, value));
}

BTW, comments welcome on if there is a hidden cost to using this approach.

EDIT: Remove names from global scope.

Just a quick comment regarding global namespace. In general I would use:

struct KeyValueTraits
{
  typedef boost::tuple<int, int> Type;
  enum {
    KEY
    , VALUE
  };
};

void foo (KeyValueTuple::Type & p) {
    p.get<KeyValueTuple::KEY> () = 0;
    p.get<KeyValueTuple::VALUE> () = 0;
}

It does look to be the case that boost::fusion does tie the identity and value closer together.

Richard Corden
  • 21,389
  • 8
  • 58
  • 85
  • Urgh. KEY and VALUE are now in global namespace. While possibly more readable, I don't think this is any less error-prone. – Roddy Jun 25 '09 at 09:46
  • @Roddy: Your point about KEY, VALUE in the global namespace is valid, and I've updated my answer to reflect an alternative. However, can you explain why you feel this is not less error-prone? – Richard Corden Jun 25 '09 at 10:08
  • @Richard: "0" and "1" are pretty definitive. p.get<0>() = 0; is ugly but unambiguous. However if i have (for example) const int KEY = 1, followed by your snippet, I can subsequently write p.get() = x; without realising that I'm using the wrong "KEY". – Roddy Jun 25 '09 at 10:39
  • @Roddy: Its possible to create lots of examples where hiding one name with anohter results in "things going wrong". However, I'm very impressed with boost::fusion, and as soon as I can work out why make_map isn't compiling I'll probably move my tuple examples to use fusion maps. – Richard Corden Jun 25 '09 at 11:08
  • That's a good way to more human-friendly meaning to a tuple. I like it. – Robert Gould Jun 26 '09 at 07:46
2

You could use boost tuples, but they don't really alter the underlying issue: Do your really want to access each part of the pair/tuple with a small integral type, or do you want more 'literate' code. See this question I posted a while back.

However, boost::optional is a useful tool which I've found replaces quite a few of the cases where pairs/tuples are touted as ther answer.

Community
  • 1
  • 1
Roddy
  • 66,617
  • 42
  • 165
  • 277
1

As Alex mentioned, std::pair is very convenient but when it gets confusing create a structure and use it in the same way, have a look at std::pair code, it's not that complex.

stefanB
  • 77,323
  • 27
  • 116
  • 141
1

I don't like std::pair as used in std::map either, map entries should have had members key and value.
I even used boost::MIC to avoid this. However, boost::MIC also comes with a cost.

Also, returning a std::pair results in less than readable code:

if (cntnr.insert(newEntry).second) { ... }

???

I also found that std::pair is commonly used by the lazy programmers who needed 2 values but didn't think why these values where needed together.

stefaanv
  • 14,072
  • 2
  • 31
  • 53