Reputation: 4367
This question (Why does INVOKE facility in the C++11 standard refer to data members?) asks why INVOKE discusses data members but ignores how they are actually invoked.
This question (What is std::invoke in c++?) discusses why they are accessed, but why they are not called if callable.
Define INVOKE(f, t1, t2, …, tN) as follows:
- (1.1) (t1.*f)(t2, …, tN) when f is a pointer to a member function of a class T and is_base_of_v<T, remove_reference_t<decltype(t1)>> is true;
- (1.2) (t1.get().*f)(t2, …, tN) when f is a pointer to a member function of a class T and remove_cvref_t<decltype(t1)> is a specialization of reference_wrapper;
- (1.3) ((*t1).*f)(t2, …, tN) when f is a pointer to a member function of a class T and t1 does not satisfy the previous two items;
- (1.4) t1.*f when N == 1 and f is a pointer to data member of a class T and is_base_of_v<T, remove_reference_t<decltype(t1)>> is true;
- (1.5) t1.get().*f when N == 1 and f is a pointer to data member of a class T and remove_cvref_t<decltype(t1)> is a specialization of reference_wrapper;
- (1.6) (*t1).*f when N == 1 and f is a pointer to data member of a class T and t1 does not satisfy the previous two items;
- (1.7) f(t1, t2, …, tN) in all other cases.
1.4 through 1.6 deal with access to pointers to data members, which makes sense given functors and stored callables. What I don't understand is why it doesn't call those members, but instead simply dereferences them? I would expect that 1.4 would parallel the syntax of 1.1 and er...invoke the object in question if f
possesses an operator ()
.
Why is this restriction in place, and what purpose does it serve?
Here's some code for clarification:
#include <functional>
#include <iostream>
struct func1
{
void operator()() { std::cout << "Invoked functor\n"; }
};
void func2()
{
std::cout << "Invoked free function\n";
}
struct D1 {};
struct T1 {
func1 f1;
void func3() { std::cout << "Invoked member function\n"; }
D1 d1;
};
int main()
{
T1 t1;
func1 free_f1;
std::invoke(&T1::f1, t1); //does nothing
std::invoke(&func1::operator(), t1.f1); //okay, so there is a workaround, if clumsy
std::invoke(&func2); //calls func2
std::invoke(&T1::func3, t1); //calls func3
std::invoke(&T1::d1, t1); //does nothing (expected)
std::invoke(free_f1); //works on non-member functors
return 0;
}
This compiles nicely, but only calls func1()
on the second call to invoke
. I understand why INVOKE does nothing but dereference the first argument when it is not a callable object. My question is why does the standard not allow for calling callable pointers to data members, i.e. why does the Standard not mandate that f1
is called in the first usage of std::invoke
above?
Edit: since std::invoke
was added in C++17, I'm tagging this question as such hoping someone involved in the process can shed some light.
Here's the original paper for adding std::invoke()
, which actually explains in its motivation that it wants to handle functors uniformly:
Although the behaviour of the INVOKE expression may be reproduced by combination of the existing standard library components, separate treatment of the functors and member pointers is required in such solutions.
In the code above, you can see this works...just not for pointers to member data that are functors themselves. Is this simply an oversight?
Upvotes: 2
Views: 215
Reputation: 473966
When C++11's standard library was being assembled, it took on a number of features from a variety of Boost libraries. For the purpose of this conversation, the following Boost tools matter:
bind
function
reference_wrapper
mem_fn
These all relate to, on one level or another, a callable thing which can take some number of arguments of some types and results in a particular return value. As such, they all try to treat callable things in a consistent way. C++11, when it adopted these tools, invented the concept of INVOKE
in accord with perfect forwarding rules, so that all of these would be able to refer to a consistent way of handling things.
mem_fn
's sole purpose is to take a pointer to a member and convert it into a thing directly callable with ()
. For pointers to member functions, the obvious thing to do is to call the member function being pointed at, given the object and any parameters to that function. For pointers to member variables, the most obvious thing to do is to return the value of that member variable, given the object to access.
The ability to turn a data member pointer into a unary functor that returns the variable itself is quite useful. You can use something like std::transform
, passing it mem_fn
of a data member pointer to generate a sequence of values that accesses a specific accessible data member. The range features added to C++20 will make this even more useful, as you can create transformed ranges, manipulating sequences of subobjects just by getting a member pointer.
But here's the thing: you want this to work regardless of the type of that member subobject. If that subobject just so happens to be invokable, mem_fn
ought to be able to access the invokable object as though it were any other object. The fact that it happens to be invokable is irrelevant to mem_fn
's purpose.
But boost::function
and boost::bind
can take member pointers. They all based their member pointer behavior on that of boost::mem_fn
. Therefore, if mem_fn
treats a pointer to a data member as a unary functor returning the value of that member, then all of these must treat a pointer to a data member that way.
So when C++11 codified all of these into one unifying concept, it codified that practice directly into INVOKE
.
So from an etymological perspective, that is why INVOKE
works this way: because all of these are meant to treat callables identically, and the whole point of mem_fn
with regard to data member pointers is to treat them as unary functions that return their values. So that's how everyone else has to treat them too.
And isn't that a good thing? Is that not the correct behavior? Would you really want to have pointers to data members behave wildly differently if the type the member pointer points to happens to be callable than if it doesn't? That would make it impossible to write generic code that takes data member pointers, then operates on some sequence of objects. How would you be able to generically access a data member pointer, if you weren't sure it would get the subobject being reference or invoke the subobject itself?
Upvotes: 3