Categories
C++Programming

Requires clause for overload resolution – real-world example

Intro

This is the third (and probably last) article of my small mini series about maybe not so well known C++ features / pattern / programming techniques for which I found a practical usage during programming on my private projects.

This time the topic is about the requires clause for overload resolution.

The requires clause

“The keyword requires is used to introduce a requires-clause, which specifies constraints on template arguments or on a function declaration.”

From cppreference.com

A simple example may look like this:

template< typename T >
requires std::is_floating_point_v<T>
void calc( T x )  // all floating point types go here...
{
}

C++20 also has concepts, which should be used for specify possible template types. It is possible to combine concepts and the usage of the requires clause:

// T must fulfill the concept of integral, but ...
template< std::integral T >
requires( !std::is_same_v<T, bool> && !std::is_same_v<T, wchar_t>
      && !std::is_same_v<T, char16_t> && !std::is_same_v<T, char32_t> 
      && !std::is_same_v<T, char8_t> )
void calc( T x )  // ... we don't want bool and character types!
{
}

// NOTE: The requires clause is a little bit simplified.
//       For match all possibilities std::remove_cvref_t<T> 
//       must be used for compare in std::is_same_v<>.

Requires clause for overload resolution

As it is illustrated in the examples above, the requires clause can be used to restrict the possible types and/or pick the desired types.

But it can also be used to pick the desired/correct function overload.

Sometimes the compiler don’t choose the same overload as the programmer expects. And sometimes the compiler even cannot choose one overload because of an ambiguity.

For the first case I made a godbolt example, which I will explain here.
For the latter I will then provide my real-world example from my private projects.

Tell the compiler which overload to pick.

You can try the next code example on godbolt by your self. Here is the link: https://godbolt.org/z/1d6EWj5P7

// Imagine you have these 2 functions:

void test( int x )
{
    std::cout << "test( int ) "  << x << std::endl;
}

void test( short x )
{
    std::cout << "test( short ) "  << x << std::endl;
}

// now you want call it with a variable of type char.

char c = 123;

// call it:
test( c );

// which overload will be called ???

If we only consider the size perspective, the short overload should be called. Also, if there are only these 2 function overloads, the intention of the programmer was most likely exactly that. But the rules of C++ are different. For smaller integral types than int, the default promotion will be always int. So, in this case the int overload will be called.

We can fix this by make the call explicit: test( static_cast<short>(c) );.

But we must do that all the time and it can be easily forgotten.

We can solve this perfectly with the help of templates and the requires clause:

template< std::integral T>
void test3( T x )
{
    std::cout << "test3(std::integral): "  << x << std::endl;
}

template< std::integral T>
requires ( sizeof(T) < 4 )
void test3( T x )
{
    std::cout << "test3(sizeof < 4): "  << x << std::endl;
}

// now you want call it with a variable of type char.

char c = 123;

// call it:
test3( c );

As you can verify on godbolt, now the correct overload (with the better fitting type size) will be called. All integral types smaller 4 bytes (e.g., short and char) will use the second overload, while all integral types with a size equal or bigger than 4 bytes will use the first overload.

Even in another example, if we have an overload with unsigned char, the (for us) desired overload will not be called.

void test2( int x )
{
    std::cout << "test2( int ) "  << x << std::endl;
}

void test2( unsigned char x )
{
    std::cout << "test2( unsigned char ) "  << x << std::endl;
}

// now you want call it with a variable of type char.

char c = 123;

// call it:
test2( c );

// Do you believe the second overload will be called ??

The second overload will not be called, even though the types differ only in the signedness.

Again, templates and the requires clause are our friends here:

template< std::integral T>
void test4( T x )
{
    std::cout << "test4(std::integral): "  << x << std::endl;
}

template< std::integral T>
requires ( sizeof(T) == 1 && std::is_unsigned_v<T> )
void test4( T x )
{
    std::cout << "test4(sizeof == 1 && unsigned): "  << x << std::endl;
}

template< std::integral T>
requires ( sizeof(T) == 1 && std::is_signed_v<T> )
void test4( T x )
{
    // (could do some checks here first.)
    std::cout << "test4(sizeof == 1 && signed): now calling unsigned overload." << std::endl;
    test4( static_cast<std::make_unsigned_t<T>>(x) );
}


// now you want call it with a variable of type char.

char c = 123;

// call it:
test4( c );

Now we can even do some extra stuff for signed types (if needed) and all will be automatically called correctly.

Resolve ambiguity

Sometimes there is an ambiguity for pick an overload and the compiler cannot choose one.

This happened during my early development of TeaScript.

In TeaScript I have the Content class which is a little bit like a std::string_view but with a moving current position and maintaining a line and column counting within the string content. This class is used by the Parser for parse through script code.

You can view the class on Github: class Content.

My goal was to make it easily usable with any (8bit/utf-8) string content. So, there is of course a constructor taking a std::string as well as a std::string_view. But I also added an constructor taking an fixed size C-array of chars:

template< size_t N>
Content( char const (&content)[N] )
    : Content( &content[0], N )
{
}

This is very useful and handy for call it with (raw-)string literals:

// No string object constructed here.
// Also, the compiler puts the correct string length at compile time.
auto const c = Content( "const dice_roll := rolldice( 20 )" );

Then, just for the sake of completeness and for support older code, I wanted to provide an overload just with a char const *.
I already had the constructor taking 2 parameters, the char const * and a size_t for the length.
But that was not sufficient for me. Why? Because the parameter for the length has to be passed by the user. That is error prone. It is easy to make a mistake by accident. Also, it is unhandy, because the user has to call strlen by itself and the user must read the docu if there are special requirements (e.g., length with or without trailing 0, etc…).
So, I wanted just a constructor with one pure plain char const * for I take all the work away from the user as a convenience.

But I had bad luck. The compiler did not accept that constructor because it is ambiguous with the fixed size array overload from above.
This is because of the array to pointer decay. Any C-array will automatically decay to a plain pointer of the same element type. This results in the ambiguity for the compiler.

Happily I could solve this with the help of a template and the requires clause:

template< typename CharPtr = char const * >
Content( CharPtr pStr ) requires std::is_pointer_v<CharPtr>
    : Content( pStr, ::strlen(pStr) + 1 )
{
}

With that it was possible for the compiler to automatically choose the correct overload. 🙂

Conclusion / Final thoughts

Personally I think, the requires clause is a great advantage in modern C++ programming and there are plenty of useful things for it.

What do you think of this technique and/or this concrete real-world example?
I hope you enjoyed reading this article. Thank you for reading. 🙂

One reply on “Requires clause for overload resolution – real-world example”

Leave a Reply

Your email address will not be published. Required fields are marked *