40

I was perusing section 13.5 after refuting the notion that built-in operators do not participate in overload resolution, and noticed that there is no section on operator->*. It is just a generic binary operator.

Its brethren, operator->, operator*, and operator[], are all required to be non-static member functions. This precludes definition of a free function overload to an operator commonly used to obtain a reference from an object. But the uncommon operator->* is left out.

In particular, operator[] has many similarities. It is binary (they missed a golden opportunity to make it n-ary), and it accepts some kind of container on the left and some kind of locator on the right. Its special-rules section, 13.5.5, doesn't seem to have any actual effect except to outlaw free functions. (And that restriction even precludes support for commutativity!)

So, for example, this is perfectly legal:

#include <utility>
#include <iostream>
using namespace std;

template< class T >
T &
operator->*( pair<T,T> &l, bool r )
    { return r? l.second : l.first; }

template< class T >
 T & operator->*( bool l, pair<T,T> &r ) { return r->*l; }

int main() {
        pair<int, int> y( 5, 6 );
        y->*(0) = 7;
        y->*0->*y = 8; // evaluates to 7->*y = y.second
        cerr << y.first << " " << y.second << endl;
}

It's easy to find uses, but alternative syntax tends not to be that bad. For example, scaled indexes for vector:

v->*matrix_width[2][5] = x; // ->* not hopelessly out of place

my_indexer<2> m( v, dim ); // my_indexer being the type of (v->*width)
m[2][5] = x; // it is probably more practical to slice just once

Did the standards committee forget to prevent this, was it considered too ugly to bother, or are there real-world use cases?

Community
  • 1
  • 1
Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • A data point: Coneau (http://www.comeaucomputing.com/tryitout/) rejects your code even after I have removed `` and the first operator: `error: no operator "->*" matches these operands` – sbi Apr 23 '10 at 07:31
  • 1
    @sbi: Comeau was the first place I went to try and see. I still have that tab open… the only code I put in before switching to GCC 4.5 was "struct x { int y; }; int &operator->*( x &l, int r ) { return l.y; } void f() { x q; int &i = q->*3; }". — and it returns success for the main example minus everything dependent on that first type_traits dependent overload. – Potatoswatter Apr 23 '10 at 07:40
  • I don't know why operator->* can be overloaded like that, but it sure looks hella ugly! I'd stay away from it for the same reason as overloading comma - it doesn't look like intuitive C++. – AshleysBrain Apr 23 '10 at 13:07

4 Answers4

23

The best example I am aware of is Boost.Phoenix, which overloads this operator to implement lazy member access.

For those unfamiliar with Phoenix, it is a supremely nifty library for building actors (or function objects) that look like normal expressions:

( arg1 % 2 == 1 )     // this expression evaluates to an actor
                 (3); // returns true since 3 % 2 == 1

// these actors can also be passed to standard algorithms:
std::find_if(c.begin(), c.end(), arg1 % 2 == 1);
// returns iterator to the first odd element of c

It achieves the above by overloading operator% and operator==. - applied to the actor arg1 these operators return another actor. The range of expressions which can be built in this manner is extreme:

// print each element in c, noting its value relative to 5:
std::for_each(c.begin(), c.end(),
  if_(arg1 > 5)
  [
    cout << arg1 << " > 5\n"
  ]
  .else_
  [
    if_(arg1 == 5)
    [
      cout << arg1 << " == 5\n"
    ]
    .else_
    [
      cout << arg1 << " < 5\n"
    ]
  ]
);

After you have been using Phoenix for a short while (not that you ever go back) you will try something like this:

typedef std::vector<MyObj> container;
container c;
//...
container::iterator inv = std::find_if(c.begin(), c.end(), arg1.ValidStateBit);
std::cout << "A MyObj was invalid: " << inv->Id() << std::endl;

Which will fail, because of course Phoenix's actors do not have a member ValidStateBit. Phoenix gets around this by overloading operator->*:

(arg1 ->* &MyObj::ValidStateBit)              // evaluates to an actor
                                (validMyObj); // returns true 

// used in your algorithm:
container::iterator inv = std::find_if(c.begin(), c.end(), 
      (arg1 ->* &MyObj::ValidStateBit)    );

operator->*'s arguments are:

  • LHS: an actor returning MyObj *
  • RHS: address of a member

It returns an actor which evaluates the LHS and looks for the specified member in it. (NB: You really, really want to make sure that arg1 returns MyObj * - you have not seen a massive template error until you get something wrong in Phoenix. This little program generated 76,738 characters of pain (Boost 1.54, gcc 4.6):

#include <boost/phoenix.hpp>
using boost::phoenix::placeholders::arg1;

struct C { int m; };
struct D { int n; };

int main() {
  ( arg1  ->*  &D::n ) (new C);
  return 0;
}
Kietz
  • 1,186
  • 11
  • 19
  • Excellent, thank you for sharing! However this usage suggests an implementation as a canonical member function, whereas the question is more specifically about non-member (free) functions. – Potatoswatter Dec 17 '13 at 21:38
  • 1
    I checked: The Boost implementation uses a non-member function in a separate, anti-ADL namespace. This is perhaps done for general best practice and uniformity with the other operators. – Potatoswatter Dec 18 '13 at 01:50
7

I agree with you that there is an incoherence on the standard, It doesn't allows overloading of operator[] with non-member functions and allows it for operator->*. For my point of view operator[] is to arrays as operator->* is to structs/classes (a getter). Members of an array are selected using an index. Members of a struct are selected using member pointers.

The worst is that we can be tempted to use ->* instead of operator[] to get an array like element

int& operator->*(Array& lhs, int i);

Array a;

a ->* 2 = 10;

There is also another possible incoherence. We can use a non member function to overload operator+= and all the operator of the form @=) and we cannot do it for operator=.

I don't really know what is the rationale to make the the following legal

struct X {
    int val;
    explicit X(int i) : val(i) {}
};
struct Z {
    int val;
    explicit Z(int i) : val(i) {}
};
Z& operator+=(Z& lhs, const X& rhs) {
    lhs.val+=rhs.val;
    return lhs;
}

Z z(2);
X x(3);
z += x;

and forbidding

Z& operator=(Z& lhs, const X& rhs) {
    lhs.val=i;
    return lhs;
}

z = x;

Sorry to not answer to your question, but adding even more confusion.

T.C.
  • 133,968
  • 17
  • 288
  • 421
Vicente Botet Escriba
  • 4,305
  • 1
  • 25
  • 39
  • 1
    `operator ->` and `operator @=` must be member functions. Free functions may be supported by your compiler, but that is certainly nonstandard. – Potatoswatter Apr 25 '10 at 14:52
  • operator @= can be overloaded by non-member functions. See the standard. – Vicente Botet Escriba Apr 27 '10 at 06:23
  • 2
    §13.5.3 doesn't differentiate between the assignment operators. It says "an assignment operator shall be implemented by a non-static member function with exactly one parameter." §5.17 says "There are several assignment operators, all of which group right-to-left." Unless there's something I'm missing, `operator=` isn't supposed to be special. – Potatoswatter May 02 '10 at 03:58
  • 6
    The rationale for disallowing non-member `operator =` is in D&E. `operator =` is special in a sense that the compiler always provides one if you don't declare your own in the class definition. If non-member `operator =` were allowed, you could define it in the middle of the translation unit. In this case the code that resides *above* that definition would have to use the compiler-provided member `operator =` and the code *below* that definition would have to use the explicitly defined non-member `operator =` - inconsistent. To avoid the inconsistency, standalone `operator =` was disallowed. – AnT stands with Russia May 02 '10 at 04:48
  • 2
    @Potatoswatter: I remember it was covered by a defect report. Here it is actually: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#221. The 13.5.3 is intended to refer to simple assignment only. Compound assignment operators are allowed to be overloaded by non-member functions, while simple assignment must be a member. So simple assignment `operator =` is special. – AnT stands with Russia May 02 '10 at 04:52
  • @Andrey: Huh, interesting. The rationale revolves how the ARM worked around `bool` not being built-in?? I guess compatibility is important… Why does the Additional Note from October 2000 appear after the proposed resolution from April 2001? And the resolution was adopted into the FCD, but 13.5.3 wasn't changed so it's pretty much as unclear as ever… – Potatoswatter May 02 '10 at 05:17
  • Ah well, I guess `int &operator+=( int &lhs, MyExpr<…> &rhs )` is a legitimate use which relates pretty closely to the ARM's problem. I should've thought of that earlier. – Potatoswatter May 02 '10 at 05:27
  • @Potatowatter "Basic assignment" = and "assignment by operator" += follow different rules. See here . Sorry I have not found it on the standard. – Vicente Botet Escriba May 02 '10 at 08:14
  • @AndreyT operator= can also be used to define other functions than the default copy constructor. This explain the overload operator= with the same signature than the default copy constructors but not the other overloading. – Vicente Botet Escriba May 02 '10 at 08:18
  • @Potatowatter Thanks for the pointer. Does it means that C++0x will not be backward compatible with C++98, or at the end I was right about the ability of overloading by non-member functions? – Vicente Botet Escriba May 02 '10 at 08:23
  • @Vicente: I don't exactly understand what you are trying to say about copy constructors and what it has to do with `operator=` – AnT stands with Russia May 02 '10 at 08:58
  • @AndreyT Sorry, I wanted to say 'default assignment'. X& operator=(Y const&); – Vicente Botet Escriba May 02 '10 at 15:15
2

Googling around a bit, I found more instances of people asking whether operator->* is ever used than actual suggestions.

A couple places suggest T &A::operator->*( T B::* ). Not sure whether this reflects designer's intent or a misimpression that T &A::operator->*( T A::* ) is a builtin. Not really related to my question, but gives an idea of the depth I found in online discussion & literature.

There was a mention of "D&E 11.5.4" which I suppose is Design and Evolution of C++. Perhaps that contains a hint. Otherwise, I'm just gonna conclude it's a bit of useless ugliness that was overlooked by standardization, and most everyone else too.

Edit See below for a paste of the D&E quote.

To put this quantitatively, ->* is the tightest binding operator that can be overloaded by a free function. All the postfix-expression and unary operators overloads require nonstatic member function signatures. Next precedence after unary operators are C-style casts, which could be said to correspond to conversion functions (operator type()), which also cannot be free functions. Then comes ->*, then multiplication. ->* could have been like [] or like %, they could have gone either way, and they chose the path of EEEEEEVIL.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • Does this answer your question? I didn't know that we can accept our own answer :) – Vicente Botet Escriba May 02 '10 at 07:58
  • @Vicente: I considered accepting yours, but it still erroneously says that non-member `operator->` is allowed and the rationale (or 2) for allowing `operator+=` but disallowing `operator=` was worked out in the comments. And I wasn't really looking for (dis)agreement, the question was *Did the standards committee forget to prevent this, was it considered too ugly to bother, or are there real-world use cases?*, which is a matter of finding some references or evidence to support one of the conclusions. – Potatoswatter May 02 '10 at 08:11
  • I'm not really happy with this self-answer, but I was spurred on by a downvote (it may be rude to leave an open-ended question unanswered so long) and there seems to be a lack of evidence, which would support the conclusion that it was overlooked. – Potatoswatter May 02 '10 at 08:12
  • Here's the quote from D&E 11.5.4: "`operator ->*` was made overloadable primarily because there wasn't any reason not to (because of orthogonality, if you must). It turns out to be useful for expressing binding operations that somehow have semantics that parallel those of the built-in meaning for `->*`. No special rules are needed; `->*` behaves just like any other binary operator. `operator .*` wasn't included among the operators a programmer could overload for the same reason `operator .` wasn't." – Doug Oct 06 '10 at 06:06
  • Another part of D&E discusses this as well: section 3.6.2. Stroustrup explains why he banned non-member `operator=` (it was legal). It seems that around this point, he introduced overloading on `[]`, `()`, and `->` and required them to be members: "it seemed a harmless restriction that eliminated the possibility of some obscure errors because these operators invariably depend on and typically modify the state of their left-hand operand", but then: "However, it is probably a case of unnecessary nannyism." It appears that `operator->*` and `operator,` were added later and escaped this. – Doug Oct 06 '10 at 08:19
  • "All the postfix-expression and unary operators overloads require nonstatic member function signatures. " ?? `C operator-(C);` and `C operator--(C&, int)` are valid. – L. F. Jan 30 '20 at 12:24
1

Standard (Working Draft 2010-02-16, § 5.5) says:

The result of an ->* expression is an lvalue only if its second operand is a pointer to data member. If the second operand is the null pointer to member value (4.11), the behavior is undefined.

You may want this behavior to be well-defined. For example, check if it is a null pointer and handle this situation. SO I quess it is right decision for a standard to allow ->* overloading.

topright gamedev
  • 2,617
  • 7
  • 35
  • 53
  • 3
    Clause 5 doesn't apply to overloads. For example, an `operator/` overload can define behavior for its second operand being zero, and you can legally use that. – Potatoswatter Apr 23 '10 at 16:17