Can the standard allow (or would run into contradictions) calling a member function as if it was free function?
Asked Answered
S

1

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.

Storz answered 23/2, 2023 at 19:54 Comment(20)
Compilers are generally allowed to create language extensions, yeah. It's pretty common to do so.Yakut
Why not just use for_each(v, std::bind(&Foo::bar, _1));?Foolhardy
@Foolhardy 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)Pantelegraph
@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).Foolhardy
your code should works at least for std::ranges::for_each since it use invokeLeeuwarden
@Foolhardy 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.Pantelegraph
The UFCS proposal seems to have stalled.Equable
As @appleapple says, this code works already: godbolt.org/z/YsvnTqvsc . Please clarify.Rafter
@Rafter the propsal is to make for_each(v, &Foo::bar); work out of the boxBritteny
@ecatmur, in the question body, before the example I wrote higher-order functions, 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?Storz
@Yakk-AdamNevraumont, do you have an example of why std::bind is so bad?Storz
@Storz 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.Pantelegraph
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.Pantelegraph
@463035818_is_not_a_number for_each(v, &Foo::bar); does work out of the box. This question is currently lacking motivation.Rafter
@Storz 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.Rafter
@ecatmur, what about my last edit?Storz
@Storz 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?Rafter
@ecatmur, go for it. I'm without a computer for the whole weekend :/Storz
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.Flooded
See Bjarne Stustroup and Herb Sutters take on this: open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4474.pdf and open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4165.pdfGilroy
B
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.

Biotite answered 25/2, 2023 at 8:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.