DEV Community

Cover image for Improving pointer to member function ->* (very bad) syntax
Lena
Lena

Posted on

Improving pointer to member function ->* (very bad) syntax

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)();
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

So, it is not very useful except in two situations:

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?

Meme, me as Phineas saying to you as Ferb, I know what we are going to do today

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...);
Enter fullscreen mode Exit fullscreen mode

instead of that we are forced to write this:

(my_ptr->*my_ptr_on_method)(my_args...);
Enter fullscreen mode Exit fullscreen mode

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");

Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

That's nice, but could be better.

Meme with the template Wonder woman guy saying that template are nice, but it could be better with concepts

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>;
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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)
{

}
Enter fullscreen mode Exit fullscreen mode

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 [&]() {  };
}
Enter fullscreen mode Exit fullscreen mode

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) {  };
Enter fullscreen mode Exit fullscreen mode

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)...); };
Enter fullscreen mode Exit fullscreen mode

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)...);
    };
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)