On generic programming, value types and constraints

On generic programming, value types and constraints

Last week I have published a new riddle, it involved generic programming from C++20 and an interesting case of overloading and template type deduction

I have promised a full answer to this question but the answer is long and I need to use examples so it would be more readable and understandable so I have decided to illustrate it in a new article.

Let's start from the beginning.

The question at hand:

No alt text provided for this image

Breaking the question down:

The FMT library:

FMT library is an amazing print library that gives us the ability to parse strings in a python like way and can even do some of the parts in compile time, It is safe and self contained.

fmt::print:

Let's start by looking on pythons formatted strings

No alt text provided for this image

Here we can see python formatted string example. print function can take a "formatted string" the string contains {}, in each of this curly braces closure we can put a variable and this variable will be incorporated into the output string.

No alt text provided for this image

pretty nice right ?!

fmt::print gives us very similar functionality. As you can see in the example we provide something very similar to python's formatted string, but instead of putting the variables directly in the curly braces closure we are just providing the braces, and after the formatted string declaration is over we are providing the parameter that will be printed. We can provide other useful information in the curly braces, but as this article is not about FMT lib I will leave it up to the curious reader to learn more about it.

One important thing to mention about fmt lib is that it cannot print pointers that are not void*, this is important for our question.

Note 1: FMT was partially adopted into C++20 standard and we are now able to use std::format and iostream but sadly it's 2022 and there is still no implementation for it in Clang or GCC so we still need to use FMT to get all this amazing functionality.

Note 2: C++23 standard adopted the std::print and std::println that act like fmt::print but sadly I'm not sure when we will be able to use it. So FMT lib is here to stay for now.

The auto Concept:

In C++ 20 Concepts were introduced, concepts are a very powerful tool to constrain types, functions and templates (more about concepts later). One of the simplest concepts that were introduced is the auto as parameter. This is a "lightweight" concept because it implies that any type will be accepted with this function or in other words it is just like writing a templated function with type T that can be anything. With auto overloading of templated functions is simpler and we write less code.

Example of same functionality before C++20:

No alt text provided for this image

The functionality of this overloaded templated function is the same as with the auto concepts, but as mentioned above we need to declare the functions as templates and we loose some readability.

What happens in the question:

We now understand that auto in the functions is just simplified way to declare templated generic functions. As with templated function overloading the auto overloading will be the same, the compiler will try to deduce the best type and will select the most specialized function.

In this case we have created an lvalue of type int. int i = 10 and we are passing an prvalue reference to the print function. in this case the compiler looks at the first overload and tries to deduce the passed type. It sees that the type is int* and so it generates.

No alt text provided for this image

It's a valid overload because const& can implicitly accept lvalue references and rvalue references and in this case rvalue of int* is passed.

On the other hand the second instantiation it tries is:

No alt text provided for this image

The second overload will be ignored because its type is incorrect int* const* is a pointer to a pointer of int type where the value it points to is constant, and we are passing a pointer to int. (int* const* != int*).

The first overload is therefore used, with pointer type argument. In this overload we are not aware of the fact that a pointer was passed to us and we just try to print the value of the reference.

As I have mentioned before fmt lib cannot print pointers that are not void* and so the program doesn't compile. The library uses static assertion so the compiler is kind enough to give us a very clean error message.

No alt text provided for this image

How can we fix it ?

Things we shouldn't do:

  1. Because we know that this is a templated function we might be tempted to write:

No alt text provided for this image

This helps the compiler to understand that we are working with an int type and so it should generate templated versions with int. In this case it will generate this functions:

No alt text provided for this image

In this case only the second overload is applicable and we will work with the correct version of the function for pointers. But it beats the point of generic functions - the compiler should be able to deduce the correct type and generate the correct function for us. In this example it might seem acceptable to give the compiler a hint, but think what would happen if we had 5 types to specify or maybe this function would have a variadic pack with unknown number of elements.

2. You may want to do this:

No alt text provided for this image

In this case we are creating a pointer to a const memory exactly what our print function expects and we are forcing the compiler to generate this function for us:

No alt text provided for this image

But just as in the previous example we are telling the compiler what we want it to do and by that we are losing the generic part of the function.

Note: This is the same functionality but readability is even worst:

No alt text provided for this image

3. Maybe we should remove the const ?

No alt text provided for this image

In this case the correct function will be called because int* rvalue cannot be implicitly converted to int& lvalue reference. and so the only overload that is suitable is the one with int*.

Yay! we solved it! But did we solve it ?!

Sadly, we solved one problem by creating another one: the intent of these function signatures was to show that the arguments content will not be mutated during the execution of these functions, but we lost that guarantee by removing the constness.

Things we should do:

  1. Pre C++ we could constrain the types we are passing to a template with type_traits library and good old SFINE (Substitution failure is not an error).

SFINE: When creating a candidate set for?overload resolution, some (or all) candidates of that set may be the result of instantiated templates with (potentially deduced) template arguments substituted for the corresponding template parameters. If an error occurs during the substitution of a set of arguments for any given template, the compiler removes the potential overload from the candidate set instead of stopping with a compilation error, provided the substitution error is one the C++ standard grants such treatment.?If one or more candidates remain and overload resolution succeeds, the invocation is well-formed.

To achieve SFINE here we will use std::enable_if which is a great helper tool from STL to achieve SFINE.

template<bool B, class T = void>
struct enable_if {};
?
template<class T>
struct enable_if<true, T> { using type = T; };        

The first template parameter is a non-type parameter which is a Boolean condition and the second parameter is the type that will be used if the Boolean condition is satisficed. std::enable_if<B, T>::type will be well-formed if and only if the Boolean expression will be satisfied.

We will also use:

  1. std::is_pointer - Trait to see if a type is a pointer or not.
  2. std::decay - Decays all qualifiers from the type and leaves it in its most "natural" form.

Note: During our examples we will use shorter virions of traits and enable_if and is_pointer

template <bool B, class T = void
using enable_if_t = enable_if<B, T>::type

template <typename T>
using is_pointer_v = is_pointer<T>::value        
No alt text provided for this image

In this code snippet we see the combination of all the tools that were described above. We are telling the compiler to remove an overload if the decayed type is a pointer. Basically we are forcing the compiler to use the correct type deduction by removing the incorrect function overload.

2. Since C++20 Concepts are available and we can create better and cleaner constraints on types and help the compiler to select the correct overload.

We need to create a Concept:

No alt text provided for this image

For this simple concept we are creating a requires expression that accepts a parameter of type T. This concept will be satisfied only if t can be dereferenced.

Now we need to use it:

No alt text provided for this image

In this usage we are constraining the auto concept to accept only pointers. As mentioned above auto concept will accept any type but when we are using a concept on auto we are constraining the types that are accepted by the auto. The functionality is similar to what we saw with std::enable_if but it's more readable and maintainable.

If you would like to know more about Generic programming and Concepts you may watch my talk from CppCon 2022 "From templates to concepts : the amazing journey of metaprogramming".

Conclusions:

When working on generic code is very important to think about the edge cases and understand how the generic function work in the standard. What will be the deduced type, and which overload will be selected. It's important to express exactly what you want the compiler to do but always prefer the more generic way. We should always be as explicit as possible - the standard library has the tools for it like we saw above and much more than that. In addition since C++20 we have Concepts that make generic code in C++ readable and maintainable.

Amichai Oron

UX/UI SAAS Product Designer & Consultant ?? | Helping SAAS / AI companies and Startups Build Intuitive, Scalable Products.

4 个月

???? ??? ?? ?? ???????? ??? ????? ???? ?????? ???: ?????? ????? ??? ??????? ?????? ??????, ?????? ?????? ??????,?????? ????? ????????. https://chat.whatsapp.com/IyTWnwphyc8AZAcawRTUhR

回复
Asaf hakun

Software Technical Lead at General Motors

2 年

Great article! One question: Using concepts in the last example makes sure that int* won't be deduced for the first signature?

回复

要查看或添加评论,请登录

Alex Dathskovsky ?的更多文章

  • Nth element variadic pack extraction

    Nth element variadic pack extraction

    Last week I wrote about the incredible way Clang offers the Nth element from a variadic pack. As promised, I will show…

    9 条评论
  • To Int or To UInt, This Is The Question

    To Int or To UInt, This Is The Question

    During our work we use integral types to perform arithmetic calculations but usually we don't think how the selection…

    11 条评论
  • The Awesome Power of Concepts: Specialization Detection

    The Awesome Power of Concepts: Specialization Detection

    Hi C++ fans! Today I want to talk about Concepts and how they can make our lives easier when it comes to template…

    14 条评论
  • Generic Tuple Hashing With Modern C++

    Generic Tuple Hashing With Modern C++

    The Beauty of C++17 is its elegance and simplicity. A few days ago someone from my team asked me: "How can I use a…

    9 条评论

社区洞察

其他会员也浏览了