4

A member function pointer must be invoked using the .* (or ->*) syntax, so it can't be passed to a higher-order function:

#include <vector>

void for_each(auto const& v, auto f) {
  for (auto const& e : v)
    f(e);  // error: must use '.*' or '->*' to call pointer-to-member function in 'f (...)', e.g. '(... ->* f) (...)'
}

struct Foo {
  void bar();
};

int main() {
  std::vector<Foo> v(10);
  for_each(v, &Foo::bar);  // from here
}

The C++ Standard Library has two separate solutions to this: either I can use std::mem_fn() to get a free-function-like callable from a member function:

int main() {
  std::vector<Foo> v(10);
  for_each(v, std::mem_fn(&Foo::bar));  // OK
}

Or I can augment the higher-order function to use std::invoke (std::for_each already does this) instead of invoking the callable directly:

void for_each(auto const& v, auto f) {
  for (auto const& e : v)
    std::invoke(f, e);  // OK
}

But, since the syntax (&Foo::bar)(Foo{}) is invalid at the current time, couldn't the standard make it valid and equivalent to calling std::mem_fn() first on the &Foo::bar?

Effectively, this would mean "absorbing" the std::mem_fn() utility in the language.

Would that be possible? Or, would it have undesired side effects? I can't see how it could break anything, considering that it's currently invalid syntax.


As I wrote the question, a possible answer came to my mind: SFINAE could be relying on that syntax being invalid.

It the following snippet, for instance, the second static_assert would fail if the standard started to allow calling (&Foo::bar)(Foo{}):

#include <type_traits>
#include <vector>

struct Foo {
  void bar();
};

template<typename F, typename = void>
struct Trait : public std::false_type {};

template<typename F>
struct Trait<F, std::void_t<decltype(std::declval<F>()(std::declval<Foo>()))>>
    : public std::true_type {};

auto constexpr freeBar = [](Foo){};

int main() {
  static_assert(Trait<decltype(freeBar)>::value);
  static_assert(!Trait<decltype(&Foo::bar)>::value);
}

However, in the comments to my delete self-answer it was pointed out that this cannot be a reason to prevent the standard from adopting the syntax I'm thinking about.

After all, and more in general, if we wanted not to break code which uses SFINAE to detect invalid code, we could practically not add anything to the standard.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
Enlico
  • 23,259
  • 6
  • 48
  • 102
  • Compilers are generally allowed to create language extensions, yeah. It's pretty common to do so. – user253751 Feb 23 '23 at 20:01
  • Why not just use `for_each(v, std::bind(&Foo::bar, _1));`? – sklott Feb 23 '23 at 20:14
  • @sklott Well, using `std::bind` is toxic, so that is a reason not to. (`std::bind` makes that mean FAR MORE than you intend it to, involving the semantics of recursive calls to bind. Use of bind in generic code is a BAD IDEA) – Yakk - Adam Nevraumont Feb 23 '23 at 20:15
  • @Yakk-AdamNevraumont I don't know why you think that, but in this case you can use lambda. Both `bind` and lambda optimizes pretty well (and usually to the same code). – sklott Feb 23 '23 at 20:17
  • 1
    your code should works at least for `std::ranges::for_each` since it use `invoke` – apple apple Feb 23 '23 at 20:24
  • @sklott Very few people are aware of the quirks in `std::bind`. Those often include people who have tried to implement it. At least some of those that have implemented it advise against using it, because full awareness of those quirks requires a lot of effort. And lambdas solve the same problem without having to know about those quirks. And you need to know how lambdas work anyhow, so why learn two sets of quirks? Look into how bind placeholders and passing bind objects to bind work: in generic code things can go insanely wonky. Now specify what your code that uses bind acts like... heh. – Yakk - Adam Nevraumont Feb 23 '23 at 20:24
  • The [UFCS](https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax#C++_proposal) proposal seems to have stalled. – Eljay Feb 23 '23 at 20:27
  • As @appleapple says, this code works already: https://godbolt.org/z/YsvnTqvsc . Please clarify. – ecatmur Feb 23 '23 at 20:31
  • @ecatmur the propsal is to make `for_each(v, &Foo::bar);` work out of the box – 463035818_is_not_an_ai Feb 23 '23 at 20:53
  • @ecatmur, in the question body, before the example I wrote _higher-order function**s**_, in general. Apparently I picked up the wrong one. But I guess we can imagine a `for_each` old implementation, or another algorithm altogether which doesn't internally use `std::invoke`, right? – Enlico Feb 23 '23 at 21:04
  • @Yakk-AdamNevraumont, do you have an example of why `std::bind` is so bad? – Enlico Feb 23 '23 at 21:15
  • @Enlico Pass the return value of `std::bind` as an argument to a `std::bind` expression. Now explain in a simple short sentence what happens. If you cannot fully explain that (and if you don't think anything strange happens, you are wrong), don't use `std::bind` in generic code, as you don't understand how it works. If you can, wow, that is something I can't do. And you'll be qualified to correct me. `std::bind` isn't *bad*, it has *quirks*, and *nobody should bother learning those quirks*. And if you don't know the quirks, *you shouldn't use it*. – Yakk - Adam Nevraumont Feb 23 '23 at 21:32
  • 1
    Back to the OP -- look into papers on "uniform call syntax". These papers go as far as to propose `method_name(foo)` would work. They'd also do stuff with member function pointers and have been well vetted on the subject. – Yakk - Adam Nevraumont Feb 23 '23 at 21:37
  • 1
    @463035818_is_not_a_number `for_each(v, &Foo::bar);` does work out of the box. This question is currently lacking motivation. – ecatmur Feb 23 '23 at 22:54
  • 1
    @Enlico sure, I can imagine a higher-order function which hasn't been written to use `std::invoke`, but so can you! So why not give a concrete example, and give this question motivation? Right now anyone who reads it has to guess. – ecatmur Feb 23 '23 at 23:40
  • @ecatmur, what about my last edit? – Enlico Feb 24 '23 at 09:32
  • @Enlico better, but it's still not clear what you're asking. The structure I would use is: a) show an MCVE (e.g. with a naïve implementation of for_each) b) explain that std::mem_fn fixes it c) explain that std::invoke fixes it d) ask whether it could be made to Just Work in the language itself, instead. Think of how you'd introduce an EWG paper asking for this change. If you're OK with it I can edit? – ecatmur Feb 24 '23 at 13:34
  • @ecatmur, go for it. I'm without a computer for the whole weekend :/ – Enlico Feb 24 '23 at 16:47
  • 1
    IMHO, pointers to members are a misfeature, and I get the feeling the committee feels the same. We have library methods to deal with pointers to members with sanity (we can get a functor via `std::mem_fn`, or use `std::invoke` and support a certain uniform "invokable" interface), but at the core language level it might be best to let them lie. – HTNW Feb 24 '23 at 19:17
  • See Bjarne Stustroup and Herb Sutters take on this: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4474.pdf and https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4165.pdf – joergbrech Feb 24 '23 at 20:40

1 Answers1

1

The idea of treating a non-static member function as an ordinary function with the additional object parameter (as done, say, by Python's C.f syntax) has of course been proposed multiple times and has been the subject of questions here. One complication is that a pointer-to-member can refer to a virtual function and calls the most-derived function; allowing a pointer-to-member to be called would presumably perform dynamic dispatch on the first argument, which would be novel (albeit without "contradicting" anything).

C++23 actually provides this for explicit-object member functions that cannot be virtual by providing ordinary pointers to functions for them:

struct A {
  void f(this A&);
};
void (*fp)(A&)=A::f;     // OK
void (A::*pmf)()=&A::f;  // error: initializer has wrong type

Meanwhile, it's important to note that the syntax (&Foo::bar)(Foo{}) is not prima facie invalid: C++20 says that overload resolution proceeds for it as if it were f.bar(Foo{}) for some f of type Foo ([over.match.call.general]/2). Of course, this call cannot actually be made, so performing overload resolution as if it could (potentially triggering ambiguities with static member functions with one more parameter) is not very helpful. Moreover, neither GCC nor Clang implements that overload resolution properly (instead claiming that [over.over] applies and fails), so C++23 changes overload resolution for that syntax to instead be compatible with the potential(!) feature of calling a pointer-to-member function.

So the (perhaps unsatisfying) answer is that there are real challenges in making such a change work, but there has also been some progress in addressing them.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76