Barry Revzin
[email protected]
Make Pointers to Member Functions Callable Introduction
Member functions play an important role in C++. However, pointers to members are still second-class citizens to pointers to non-members. They’re INVOKE-able, but not actually invokable. As a result, lots of code exists to handle this exceptional case. But there’s nothing exceptional about pointers to member functions. Or at least, there shouldn’t be.
Motivation Consider the following declarations: struct X { int member(int ); };
int non_member(X*, int);
auto p_mem = &X::member; auto p_fun = &non_member;
While the functions non_member() and member() have different rules for accessing members of X, on the call side, they’re both still functions. And yet, we have radically different syntax rules for simply calling them: X x; (x.*p_mem)(1); p_fun(&x, 2);
The second one is just a normal function call, the first one is a mess. The syntax is difficult for beginners to grok, and even experienced C++ programmers find it cryptic. This different call syntax also means that we can’t use pointers to member functions directly in any of the standard algorithms. As Scott Meyers laments in Item 41 of Effective STL:
In a perfect world, I’d also be able to use for_each to invoke Widget::test on each object in vw: for_each(vw.begin(), vw.end(), &Widget::test); // Call #2 (won’t compile)
In fact, if the world were really perfect, I’d be able to use for_each to invoke Widget::test on a container of Widget* pointers, too:
list
lpw; for_each(lpw.begin(), lpw.end(), &Widget::test); // Call #3 (also won’t compile)
For these cases, we now have std::mem_fn. But that’s boilerplate that just hides what is going on, and hardly makes for a “perfect world.” There already exists a clear desire to make pointers to member functions more uniform with all the other INVOKE-able objects. We can put either member functions or non-member functions inside of a std::function, or passed to std::bind or the newly accepted std::invoke: std::function f1 = &X::member; std::function f2 = non_member; int b1 = std::bind(&X::member, &x, 1)(); int b2 = std::bind(non_member, &x, 2)(); int i1 = std::invoke(&X::member, &x, 1); int i2 = std::invoke(non_member, &x, 2);
In Eric Niebler’s range-v3 library, the predicates and functions passed to algorithms are wrapped in the library side so that the difference is transparent to the user. But all of these cases simply shift the burden from the user to the library developer to handle this case separately. It would be far simpler to just do away with the special case entirely.
Proposal Rather than either placing the burden on the user to know how to handle pointers to member functions, or placing the burden on the library implementers to take care of everything for the user, or suggesting to rewrite everything in , this paper is simply proposing to make all INVOKE-ables actually invokable by supporting the “normal” call syntax for pointers to member functions. That is: X x; p_mem(&x, 1); // equivalent to (x.*p_mem)(1);
The same would apply to pointers to member data: struct Y { int val; };
auto p_val = &Y::val; Y y{42}; p_val(y) = 17; // equivalent to (y.*p_val) = 17; // now y.val == 17
This syntax is natural and intuitive. You already know how to interpret it correctly. There are no surprises here. [func.require] currently defines the concept of INVOKE. I am simply proposing that we equate the meaning of f(t1, t2, ..., tN) with INVOKE(f, t1, t2, ..., tN).
Note that this proposal is not related to the unified call syntax proposal. The latter proposal is about supporting the syntax member(&x, 1) – calling member functions by name using the non-member call syntax. This proposal is about calling pointers to members.
Impact In most cases, the adoption of this proposal will not affect existing code. This paper is not proposing to remove the current pointer-to-member call syntax. The implementations of the aforementioned std::function, std::bind, and std::invoke could change to be simpler (but don’t have to). std::mem_fn would effectively become obsolete. There may be examples in code which use SFINAE to determine if an object is callable that previously rejected pointers to member functions which may now break. This paper suggests that the breakages, such as they are (and there may not be many), are worth it in order to simplify the language rules. The syntax becomes more regular, more predictable, more uniform.