Article::Article
Not so long ago, I was bored, so I went to browse cppreference. Yes I do that, I know it's weird but don't mock my special interest!
Anyway, I chose to go to the Function objets without any real motive, except maybe to see if std::move_only_function, added in C++23 was already documented in the website.
It was there, I also saw std::function obviously, but before I scrolled down to see std::bind, I stumbled upon one class I didn't recall, it's not even new, it was added in C++11. I'm talking about std::mem_fn.
What is it? Well, it's pretty simple, it's a function to create a wrapper around a pointer to method, instead of writing:
#include <iostream>
struct Foo
{
void hi()
{
std::cout << "Hi" << std::endl;
}
};
int main()
{
auto ptr_hi = &Foo::hi;
// Use .*
Foo foo;
(foo.*ptr_hi)();
// Use ->*
auto* foo_ptr = &foo;
(foo_ptr->*ptr_hi)();
}
Compiler explorer link
You can write:
#include <iostream>
#include <functional>
struct Foo
{
void hi()
{
std::cout << "Hi" << std::endl;
}
};
int main()
{
auto wrapped_hi = std::mem_fn(&Foo::hi);
Foo foo;
wrapped_hi(foo); // Work with a value
wrapped_hi(&foo); // Also work with a pointer
}
So, it is not very useful except in two situations:
- If you plan to use it with std::bind before C++ 14 (because there is no reason to use it since C++14) or with std::bind_front since C++20.
- If you want to pass a member function to a STL algorithm. There is this very good article talking about it.
I almost forgot, it also works with smart pointers without needing to call their get()
member function. So you don't have to use the operator ->*
.
But wait! std::unique_ptr and std::shared_ptr don't have an overload for this operator! The syntax is so bad, so few people use it that they did not implement it. Like seriously, when is the last time you used it?
Today, we are going to fix the operator ->*
by making a new syntax work!
The issue with the operator ->*
We could expect to be able to write something like that:
my_ptr->*my_ptr_on_method(my_args...);
instead of that we are forced to write this:
(my_ptr->*my_ptr_on_method)(my_args...);
The reason we have to put parenthesis around my_ptr->*my_ptr_on_method
is that the operator ()
with the precedence over the operator ->*
meaning that without the parenthesis, it would try to use the operator ()
of my_ptr_on_method
first and it won't work.
Yes this is something very minor, but it is still frustrating and I can fix it!
Idea
The idea will be very similar with what I did in this article where I simulate the unified call syntax.
The first part is to implement a wrapper around the pointer to member function. This wrapper will have the operator ()
overloaded and it will be able to take the same arguments as the member function it wraps, and it will return another object that I will call the proxy.
This proxy is an object that will store the argument and the wrapped member function. It will also be able, if it is provided with an object corresponding to the wrapped member function, to make the actual call.
The last part is the overload of the operator ->*
that will prove the proxy with the object.
Here what it can looks like in pseudo code:
struct Proxy
{
Proxy(member_function, args...);
auto execute(Foo f)
{
(f->*member_function)(args...);
}
// Stored information
args...;
member_function;
};
struct Wrapper
{
Wrapper(member_function);
Proxy operator()(args...)
{
return Proxy(member_function, args...);
}
member_function;
};
auto operator->*(Foo f, Proxy p)
{
return p.execute(f);
}
// Usage
auto wrapper = &Foo::print;
Foo foo;
foo->*wrapper("hi");
Why not the operator .*
?
Because it can't be overloaded.
Implementation
First, we need the wrapper and it must be constructed from a member function:
template <typename MF>
class Wrapper
{
private:
using C = helper::ClassType<MF>;
public:
Wrapper(MF member_function):
_member_function(member_function)
{}
private:
MF _member_function;
};
That's nice, but could be better.
Let's add a concept to check that the template argument is indeed a member function:
template <typename MF>
concept MemberFunction = std::is_member_function_pointer_v<MF>;
and then use it by replacing the keyword typename
by MemberFunction
:
template <MemberFunction MF>
class Wrapper
{
private:
using C = helper::ClassType<MF>;
public:
Wrapper(MF member_function):
_member_function(member_function)
{}
private:
MF _member_function;
};
Now we need to overload the operator ()
to return the proxy. It must take the arguments of the member function as parameter:
template <typename... Args>
void operator()(Args&&... args)
{
}
It must also return the proxy, but we haven't defined it yet! The simplest way to implement it is to use a lambda function, and take the arguments by capturing them in its scope:
template <typename... Args>
auto operator()(Args&&... args)
{
// [&] means it takes everything from the current scope by reference
// It will takes 'args...' and also 'this'
return [&]() { };
}
If you are following, you remember that the Proxy must be able to use the member function if provided with the right object, so let's add an argument for our lambda:
// Note that I use a template argument here to be able to use a forwarding reference
[&]<typename T>(T&& t) { };
And now call the member function:
// std::forward<Args>(args)... may seem barbaric but it just means that we forward the argument
// With the exact same type as we got them
[&]<typename T>(T&& t) { return (t.*_member_function)(std::forward<Args>(args)...); };
The last thing to do for the wrapper is to add a concept to be able to check that the arguments provided corresponds so when it fails instead of having 3 pages of unreadable errors, we get only 20 lines.
Here's the concept:
template <typename T, typename MF, typename... Args>
concept MemberFunctionArgs =
requires(T&& t, MF mf, Args&&... args)
{
(t.*mf)(std::forward<Args>(args)...);
};
Basically, the content is the same as the lambda and it returns true if this is possible.
With this concept added, the wrapper looks like that:
template <typename MF>
concept MemberFunction = std::is_member_function_pointer_v<MF>;
template <typename T, typename MF, typename... Args>
concept MemberFunctionArgs =
requires(T&& t, MF mf, Args&&... args)
{
(t.*mf)(std::forward<Args>(args)...);
};
namespace helper
{
template <typename C, typename R, typename... Args>
struct DeduceClassTypeFromMemberFunction
{
using Class = C;
DeduceClassTypeFromMemberFunction(R (C::*)(Args...)){}
};
template <MemberFunction MF>
using ClassType = typename decltype(DeduceClassTypeFromMemberFunction(std::declval<MF>()))::Class;
} // namespace helper
template <MemberFunction MF>
class Wrapper
{
private:
using C = helper::ClassType<MF>;
public:
Wrapper(MF member_function):
_member_function(member_function)
{}
template <typename... Args>
auto operator()(Args&&... args) requires MemberFunctionArgs<C, MF, Args...>
{
return [&]<typename T>(T&& t) { return (t.*_member_function)(std::forward<Args>(args)...); };
}
private:
MF _member_function;
};
The next step is to overload the operator ->*
and it is that simple:
template <typename T, typename Proxy>
constexpr auto operator->*(T&& t, Proxy&& p)
{
return p(std::forward<T>(t));
}
But with a concept to check that p(std::forward<T>(t))
is possible, it is better:
template <typename T, std::invocable<T> Proxy>
constexpr auto operator->*(T&& t, Proxy&& p)
{
return p(std::forward<T>(t));
}
Why object instead of pointers?
Because it is only possible to overload operators for objects, not pointers. So we would say that we are more fixing the operator .*
using the ->*
in this particular case and that's why the overload of the operator ->*
takes a forwarding reference T&&
and not a pointer (T*
).
You can find the whole code source on github here.
Article::~Article
Now it is possible to write that:
using cider::operator->*;
using cider::Wrapper;
struct Foo
{
void bar(int a, int b)
{
std::cout << (a + b) << std::endl;
};
};
auto wrapped = Wrapper(&Foo::bar);
Foo foo;
foo->*wrapped(1, 1);
I don't really expect this to be used in a real-world project, because this is an edge case usage and it adds some complexity for only a little gain. My point was to show that C++ is customizable, and it is not that hard to play with C++ syntax, it just needs some key concepts and some creativity. Also, the result is not necessarily ugly even if it may still seem complicated if you are not a bit familiar with templates.
Sources
- https://stackoverflow.com/questions/17363003/why-use-stdbind-over-lambdas-in-c14
- https://stackoverflow.com/questions/62807743/why-use-stdbind-front-over-lambdas-in-c20
- https://en.cppreference.com/w/cpp/utility/functional/mem_fn
- https://en.cppreference.com/w/cpp/utility/functional/bind
- https://en.cppreference.com/w/cpp/utility/functional/bind_front
- https://www.fluentcpp.com/2020/03/06/how-to-pass-class-member-functions-to-stl-algorithms/
Top comments (0)