DEV Community

Lena
Lena

Posted on

My first attempt to simulate the unified call syntax

Article::Article

Once upon a time, a young princess, in a tower protected by a dragon a dev, stuck on her gamer chair, was testing her new experimental library and wrote:

void foreach_test()
{
    std::vector<std::string> results;
    // The code where I use my library[...]
    assert(results[2].starts_with("42.42"));
}
Enter fullscreen mode Exit fullscreen mode

But dammit! She got a compile error; the starts_with member function for std::string was only added really recently with C++ 20 and no one implemented it yet.

error: 'std::string' {aka 'class std::__cxx11::basic_string<char>'} has no member named 'starts_with'
Enter fullscreen mode Exit fullscreen mode

In rage she wrote this function, named with such distinguished taste and making a body unnecessary long when it could have been a one liner:

bool dummy_not_error_proof_start_with_because_still_not_implemented_and_that_make_me_sad(std::string_view str, std::string_view substr)
{
    if (substr.size() > str.size())
        return false;

    for (std::size_t i = 0; i < substr.size(); ++i)
        if (str[i] != substr[i])
            return false;

    return true;
}
Enter fullscreen mode Exit fullscreen mode

And then used it:

void foreach_test()
{
    std::vector<std::string> results;
    // The code where I use my library[...]
    assert(dummy_not_error_proof_start_with_because_still_not_implemented_and_that_make_me_sad(results[2], "42.42"));
}
Enter fullscreen mode Exit fullscreen mode

And yes, it really happened ... to me! You can see it here in the code of my library Aggreget (It is a way less mature equivalent of MagicGet/Boost::pfr but using concepts)

It would have been nice if I could just have been able to extend simply std::string to add a simple member function, without having to create a free function, my own string class or inheriting from std::string (bad idea for several reasons including the fact that std::string destructor is not virtual).

In this article, I show you one way to fix this issue! Note the solution may be worst than the initial problem, but it will be fun! Trust me!

Unified call syntax to the rescue

Unified call syntax is something that already exist in D and in Nim, the name is kinda barbaric but it is really simple. It allows to use free function with the same syntax as member function.

Let's reuse the code from the introduction:

std::string_view str = "42.420000001";
dummy_not_error_proof_start_with_because_still_not_implemented_and_that_make_me_sad(str, "42.42");
Enter fullscreen mode Exit fullscreen mode

Could be written:

std::string_view str = "42.420000001";
str.dummy_not_error_proof_start_with_because_still_not_implemented_and(42.42);
Enter fullscreen mode Exit fullscreen mode

Neat, isn't it?

With something like that, if you see a class of which you can't change the source (like the standard library for example) but you feel that a member function is missing, you could just write a free function, have the same developer experience when using it as if it was a member function by keeping the clarity and with the auto completion of your favorite IDE/text editor.

But sadly we don't have that in C++, and the proposed paper are not progressing at all, some people don't see the appeal, some other are afraid of the retro compatibility and how it would work during the overload resolution. Honestly, I think it's a shame, but it takes more to stop me!

Trying to implement it myself without waiting C++35

The idea

First, I need to choose an operator to overload. It needs to:

  • Have a semantic that I can overload as a free function because I don't want to modify the class I want to extend.
  • Looks like the operators . or -> because it needs to be easy to understand that we are calling a function.

That's why I chose the operator ->*, normally it is done to use pointer to member function so even its normal meaning is close.

Note that the code examples in this section are not generic at all for simplicity sakes because template don't help for making something clear.

My ideal syntax when using it would be:

// Equivalent of starts_with(str, 'c')
str->*starts_with('c')
Enter fullscreen mode Exit fullscreen mode

The difficulty to do this is that the operator () has the precedence over the operator ->*, meaning that if I decompose str->*starts_with('c') it translates to:

// First step
auto tmp = starts_with('c');
// Second step
str->*tmp;
Enter fullscreen mode Exit fullscreen mode

To create my ideal syntax, I need that starts_with('c') returns something usable my the operator overload of ->*.

To do that, starts_with must be an object with the operator () overloaded, and it must return another object containing the argument, a char in this case, and the function to execute.

// My function
bool starts_with_impl(const std::string& str, char c)
{
    return (!str.empty()) && (str.front() == c);
}

// A little alias for a function returning a boolean and taking a string and char as arguments
using ptrFunc = bool (*)(const std::string&, char);

// The object we will return containing the function to execute and the char argument
struct ReturnedObject
{
    ptrFunc f;
    char c;
};


struct StartsWith
{
    // The overload of the operator ()
    ReturnedObject operator()(char c)
    {
        return ReturnedObject{ .f = starts_with_impl, .c = c };
    }
};
// Instanciate an object so we can use the operator()
StartsWith starts_with{};
Enter fullscreen mode Exit fullscreen mode

Then the operator ->*, will use this object to get the function to execute and the call it with the char argument.

// The overload
constexpr auto operator->*(const std::string& str, ReturnedObject ro)
{
    // Call starts_with_impl(str, 'c')
    return ro.f(str, ro.c);
}

// Example of usage
int main()
{
    std::string str = "col";
    bool b = str->*starts_with('c');
    std::cout << b << std::endl; // Print 1
}
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

It works! But that's a lot of code, and I can make it shorter with the power of lambdas.

My goal here is to replace the ReturnedObject by a lambda function. This lambda function will capture the char c and then will take a const std::string& as parameter and in its body it will call starts_with_impl. I will call this lambda the proxy functor, because it is a functor (lambda are just syntaxic sugar to create functors) and it will allow us to call starts_with_impl indirectly.

struct StartsWith
{
    // The overload of the operator ()
    auto operator()(char c)
    {
        // Return the "Proxy functor"
        return
            // Which is just a lambda capturing "c" and taking a const std::string& as paramenter
            [c](const std::string& str)
            {
                // And calling starts_with_impl when using it
                return starts_with_impl(str, c);
            };
    }
};
StartsWith starts_with{};
Enter fullscreen mode Exit fullscreen mode

Then in the overload of the operator ->* we just need to call the lambda:

// The overload
constexpr auto operator->*(const std::string& str, auto&& proxy_functor)
{
    // Call starts_with_impl(str, 'c')
    return proxy_functor(str);
}
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

It is possible to make it shorter, but in the next part I will focus on how to make it generic and not only work for this particular use case.

Generic implementation

Let's implement it for real and for all types! The first step is to overload the ->* as a free function. As it is generic, there is 2 template arguments:

template <typename T, typename Proxy>
constexpr void operator->*(T&& t, Proxy&& p)
{}
Enter fullscreen mode Exit fullscreen mode

Now let's fill the inside of this function by calling the proxy functor:

template <typename T, typename Proxy>
constexpr void operator->*(T&& t, Proxy&& p)
{
    p(std::forward<T>(t));
}
Enter fullscreen mode Exit fullscreen mode

Now let's handle the return:

template <typename T, typename Proxy>
constexpr auto operator->*(T&& t, Proxy&& p)
{
    return p(std::forward<T>(t));
}
Enter fullscreen mode Exit fullscreen mode

The last step is to restrict a bit this overload and make it available only if it is possible to actually call the functor with the object as first argument by using the std::invocable concept.

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

That's it, there is only 3 lines of actual code needed to make it work, amazing, isn't it?

I also added a macro named UNIC_GENERATE_PROXY to help create the functors to be used with the operator ->* but there is nothing special about, but if you are curious you can see the code here.

Usage

You can create the functor yourself and then use:

using unic::operator->*;

struct Compare
{
    auto operator()(std::string_view starter) const
    {
        return [starter](const auto& str) { return str.compare(starter); };
    }
};
Compare compare{};

std::string_view str = "lola";
bool b = str->*compare("Lolo");
Enter fullscreen mode Exit fullscreen mode

Or wrap an existing function:

using unic::operator->*;

constexpr auto find = UNIC_GENERATE_PROXY(std::ranges::find);
constexpr std::array a = { 1, 2, 3, 4, 5 };
auto it = a->*find(3);
Enter fullscreen mode Exit fullscreen mode

Or create your own function to wrap:

using unic::operator->*;

bool dummy_not_error_proof_start_with_because_still_not_implemented_and_that_make_me_sad(std::string_view str, std::string_view substr)
{
    if (substr.size() > str.size())
        return false;

    for (std::size_t i = 0; i < substr.size(); ++i)
        if (str[i] != substr[i])
            return false;

    return true;
}

constexpr auto starts_with = UNIC_GENERATE_PROXY(dummy_not_error_proof_start_with_because_still_not_implemented_and);
constexpr std::string_view str = "lola";
constexpr bool b = str->*starts_with("lol");
Enter fullscreen mode Exit fullscreen mode

A way to simulate member function for enumerations;

The operator ->* is overloadable for any enumeration. If you want to know a bit more about operator overload for enumerations, I wrote an article about it.

This means that you can use this technique to use some functions almost like member functions. For example, when using magic enum, instead of writing:

Color color = Color::RED;
auto color_name = magic_enum::enum_name(color);
Enter fullscreen mode Exit fullscreen mode

You could write:

Color color = Color::RED;
auto color_name = color->*to_string();
Enter fullscreen mode Exit fullscreen mode

Article::~Article

You can see the whole implementation here but keep in mind that this is just a proof of concept and not a mature library. I hope that it gave you some ideas or that you learned something.

Sources

Top comments (4)

Collapse
 
pauljlucas profile image
Paul J. Lucas

Did you try overloading -> (which is overloadable)?

Collapse
 
baduit profile image
Lena

It is only overloable as a member function, not as free function, it means that I need to modify the class I want to extend, it is not possible for something like std::string. Also the overload of -> is quite special (can only return a pointer or an object with an operator -> overloaded).

I managed to do something vaguely similar -> and I have an article about that soon even if that won't be the main subject.

Collapse
 
pauljlucas profile image
Paul J. Lucas

It is only overloadable as a member function, not as free function, it means that I need to modify the class I want to extend ...

Or proxy it, perhaps?

Thread Thread
 
baduit profile image
Lena

If I manipulate a proxy with the operator -> overloaded. The first issue is I need to create everytime I want to use an "extension", else I need to always be manipulating a proxy and never a std::string (or any other class I want to extend) that adds a lot of new problems.