2

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.

[func.require]:

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?

jonspaceharper
  • 4,207
  • 2
  • 22
  • 42
  • Data in general is not something that can be called. – eerorika Jan 26 '19 at 00:39
  • @eerorika stored lambdas and function objects can be. – jonspaceharper Jan 26 '19 at 00:40
  • It makes sense for all kinds of members, not only for functors. This is explained in the linked question, so I think the linked question is an exact duplicate, isn't it? – xskxzr Jan 26 '19 at 03:44
  • You can do `struct X{int n;}; std::function f(&X::n);` Now, `f(x)` returns `x.n`. Note that `&X::n` is a plain vanilla pointer to data member, not a callable. That's the usage `(1.4)` supports. `(1.5)` and `(1.6)` are left as an exercise for the reader. – Igor Tandetnik Jan 26 '19 at 04:35
  • @IgorTandetnik Yes, that's in the linked question, but it does not get me any closer to an answer to this question. – jonspaceharper Jan 26 '19 at 08:02
  • @xskxzr No, it is not. I am asking for the motivation for how pointers to data members are handled, not why they are present in the definition of *INVOKE*. – jonspaceharper Jan 26 '19 at 08:03
  • I have updated the question to hopefully address your concerns. – jonspaceharper Jan 26 '19 at 08:43
  • Roughly, `invoke(&T::data, t)` is equivalent to `invoke([](T& u) { return u.data; }, t)`. It is *not* designed to treat `data` as a callable and call it - only to retrieve its value. The idea is to accept any pointer-to-data-member as itself a callable that retrieves the value of that member. Why would you expect different behavior depending on whether that data member is, say, a plain `int`, or a class that happens to have overloaded `operator()`? – Igor Tandetnik Jan 26 '19 at 14:24
  • Say you use `invoke` to call a function that happens to return a function pointer - I assume you don't expect `invoke` to automatically call through that returned pointer as well. Same thing in `invoke(&T1::f1, t1)` case - `&T1::f1` is a callable that takes an instance of `T1` and returns an instance of `F1 `. The fact that `F1` is itself a callable is irrelevant - if you want to call it, as opposed to just obtaining its value, you have to apply `()` operator, or another layer of `invoke` – Igor Tandetnik Jan 26 '19 at 14:33
  • @IgorTandetnik I know how `std::invoke` / *INVOKE* works. If I provide it a callable `f`, logic dictates it to call that object if it is callable. I'm asking why this corner case for callable objects exists. I do not expect it to invoke the returned value of the invocation if it is callable; that makes no sense. Possible answers: 1) No one considered it, 2) It eases some aspect of metaprogramming, 3) some arcane voodoo lost to time. All of these are valid answers, if correct. – jonspaceharper Jan 26 '19 at 15:34
  • `&T1::f1` is a callable, and `invoke` "calls" it by retrieving the value of a data member. You seem to be saying that, if that member is in turn a callable, you expect `invoke` to call that, too. What if that second call also returns a callable - should `invoke` call that, too? How far down the chain of turtles do you expect it to go? – Igor Tandetnik Jan 26 '19 at 15:37
  • Precisely one. Exactly the same distance it goes it retrieving the value of a function pointer and calling it. – jonspaceharper Jan 26 '19 at 15:38
  • Well, it cannot return a "value" of a member function - a member function is not an object. So when passing a pointer-to-member-function, the only possible action is to call it. – Igor Tandetnik Jan 26 '19 at 15:39
  • @IgorTandetnik The member function pointer gets dereferenced and called. A member functor pointer just gets dereferenced. See my confusion? – jonspaceharper Jan 26 '19 at 16:14
  • @JonHarper: I don't understand why you would *expect* a pointer to a data member to invoke the value of that member as a function, rather than simply returning the value of the member from the object. "*The member function pointer gets dereferenced and called. A member functor pointer just gets dereferenced. See my confusion?*" No, because there's no such thing as a "member functor pointer"; there are member function pointers and member *data* pointers. – Nicol Bolas Jan 26 '19 at 17:46
  • @NicolBolas Isn't the point of `invoke` to call callable objects? The paper proposes adding `invoke` to handle functors, so why are member functors excluded? Why can I not `std::invoke(&T1::f1, t1/*, HypotheticalArgs...*/)` when I *can* `std::invoke(free_f1/*, HypotheticalArgs...*/)`? – jonspaceharper Jan 26 '19 at 17:51
  • @JonHarper: Because *there's no such thing as a member functor*. A pointer to a data member is a pointer to a data member. Period, the end. The question itself is based on a concept that you invented which the standard doesn't define and doesn't expect to exist. – Nicol Bolas Jan 26 '19 at 17:55
  • @NicolBolas Forgive my confusion, but what is f1 in the example above, if not a data member that can be called? `t1.f1()` is well-formed. – jonspaceharper Jan 26 '19 at 17:58

1 Answers1

3

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?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982