Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Syntax sugar for pointer-to-member works for array but not std::vector

I wrote some sample code (below) to understand C++'s pointer-to-member feature. However, I've come across a strange issue wherein the syntax

(*it).*attribute

is accepted by the compiler, but the syntax

it->*attribute

is not accepted, with the error

left hand operand to ->* must be a pointer to class compatible with the right hand operand, but is std::__1::__wrap_iter<Bowl *>

However, if I uncomment Bowl bowls[3] = and comment out std::vector<Bowl> bowls =, i.e. switch from using std::vector to using a primitive array, then both syntaxes work correctly.

From the C++ reference, I found

The expression E1->*E2 is exactly equivalent to (*E1).*E2 for built-in types

so it seems that the error has something to do with the fact that arrays are built-in types but std::vector is not. Later in that section, I found

In overload resolution against user-defined operators, for every combination of types D, B, R, where class type B is either the same class as D or an unambiguous and accessible base class of D, and R is either an object or function type, the following function signature participates in overload resolution:

R& operator->*(D*, R B::*);

but as std::vector does not define operator->* I am very confused. Why do I get this error on one syntax but not the other, and only when using std::vector instead of a primitive array?

#include <iostream>
#include <iterator>
#include <vector>

class Bowl
{
public:
  unsigned int apples;
  unsigned int oranges;
  unsigned int bananas;

  Bowl(unsigned int apples, unsigned int oranges, unsigned int bananas)
    : apples(apples), oranges(oranges), bananas(bananas)
  {
    // nothing to do here
  }
};

template <typename TClass, typename TIterator, typename TResult>
TResult sum_attribute(TIterator begin, TIterator end, TResult TClass::*attribute)
{
  TResult sum = 0;
  for (TIterator it = begin; it != end; ++it)
  {
    sum += (*it).*attribute;
    sum += it->*attribute;
  }
  return sum;
}

int main()
{
  std::vector<Bowl> bowls =
  // Bowl bowls[3] =
    {
      {1, 2, 3},
      {4, 5, 6},
      {7, 8, 9},
    };

  int num_apples = sum_attribute(std::begin(bowls), std::end(bowls), &Bowl::apples);
  int num_oranges = sum_attribute(std::begin(bowls), std::end(bowls), &Bowl::oranges);
  int num_bananas = sum_attribute(std::begin(bowls), std::end(bowls), &Bowl::bananas);

  std::cout << "We have " << num_apples << " apples, " << num_oranges << " oranges, and " <<
    num_bananas << " bananas. Now wasn't that fun?" << std::endl;
}

1 Answers

To better understand this, I think it's helpful to talk about the parallels to the * and -> operators.

If you have an iterator of class type and you write

(*itr).field

C++ interprets it to mean

itr.operator*().field

Similarly, if you write

itr->field

C++ interprets it to mean

itr.operator->().field

Notice that these are calling different overloaded operators. The first one calls operator*, and the second calls operator->. This is different than built-in types; as you noted, for built-in types the syntax

base->field

is just a shorthand for

(*base).field

There's something similar going on here when you talk about

(*itr).*memberPtr

and

itr->*memberPtr

In the first case, C++ treats (*itr).*memberPtr to mean

itr.operator*().*memberPtr

Notice that this means that the .* operator is being applied directly to the item being iterated over and that the iterator itself isn't overloading the .* operator. On the other hand, if you write

itr->*memberPtr

C++ treats it as a call to

itr.operator->*(memberPtr)

and reports an error because iterator types aren't required to - and rarely do - implement operator ->*. (In fact, I've seen exactly zero overloads of operator ->* in the course of programming in C++).

The fact that primitive types treat base->*memberPtr as (*base).*memberPtr here is irrelevant, the same reason that the fact that primitive types treat base->field as (*base).field; the compiler will not automatically generate operator* from operator-> or vice-versa.

There's a separate question about why iterators aren't required to do this, and that's something I don't have a good answer to, unfortunately. My guess is that this is a sufficiently rare case that no one thought to put it in the standard, but I'm not sure.

As for why this works for primitive arrays but not std::vector, primitive arrays you're working with raw pointers, which support the ->* operator. The iterators for std::vector aren't required to be raw pointers, and if they're objects of class type, the above reasoning explains why you shouldn't expect them to support operator ->*.

like image 81
templatetypedef Avatar answered May 19 '26 11:05

templatetypedef



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!