You all probably already have heard of C++20's ranges. With ranges-v3, Eric Niebler provided us with a solution already, independent of C++20. In this post, I like to shed some light on how C++20's ranges work and the benefits you get from them. There are multiple benefits from ranges. Today I like to talk about consistency. I assume that you already know ranges or that you can catch up quickly. I'm focussing less on the various algorithms ranges bring us, nor the pipe syntax. I like to teach you how ranges achieve consistency, what it means, and how you can apply it to your own codebase, independent from C++20. Let's get started.
What's consistency in this context?
The first question is, what is consistency? Let's have a look at the following example:
1 2 3 4 5 6 7 8 9 10 11 12
Essentially, we can see two types there
Container A and
OtherContainer C. The internals does not matter in this post. What matters is the function
begin. We see it in B as a free-function for
Container and as a member-function in
Use, we look at an abbreviated function template from C++20. For those who haven't seen this, think of it as a function template. The key here is that we don't know the type of parameter
c -- A situation we have regularly in generic code. The question now is, what is the correct way to call
begin? I'm showing you two approaches here. D does call a free function
begin, relying on overload-resolution. E, on the other hand, does explicitly call the
std version of
The issue is, we don't know which type
c is, and both attempts are good for only one of the containers. This is a usual burden in generic code. The workaround is a so-called two-step using. We use
using to bring
std::begin into the overload-set. Now, we use an unqualified call to
begin. This picks up the version in
std and the free-function we provided for
Container. In code, it looks like this:
1 2 3 4 5 6 7
Arthur O’Dwyer wrote a post about two-step with
std::swap, What is the std::swap two-step? which explains it from a different angle.
The one issue in pre C++20 is that
std::begin deals only with member-functions which brings an inconsistency. While we can get the example above working in generic code, we end up with at least three different functions being called:
In the case of the member function, when
std::begin can be used, it calls the member function for us. The inconsistency is that not all calls are routed via
std::begin. What if
std::begin does a couple of checks on the type and puts some safety measures on if these checks fail? Then we do get these benefits for
OtherContainer but not
Container. This is not only sad. It can be a nightmare to debug.
Ranges for consistency
Of course, we wouldn't talk about ranges if they would not solve this situation. Here is what you do with ranges available:
1 2 3 4 5
ranges::begin looks for free- and member-functions. This makes it so much better. But why doesn't
std::begin do the same? Well, because of ADL (argument dependent lookup). Once we provide our own free function,
begin, for a type this one beats
std::begin. Why? Because this is how ADL works (I'm not going into the details here, it could fill at least another post).
Simply use ranges in this case, and you don't need to learn the two-step using and about ADL. At this point, you can stop reading. You already learned how you could improve your code with ranges. But you like to learn more? Good. Why should only ranges do this magic?
Consistency for your code-base
Okay, we do want to get the same as ranges. We like to have a function, let's say
begin, which users can customize, but all calls should first go to our
We use the data types from before. The goal is to provide our own
begin function in the namespace
custom, giving us the same consistent behavior as ranges do.
1 2 3 4
The code above is what we like to use. Now let's see how we build
A function object to avoid ADL
The first step is to avoid ADL. It is great, but in our case effectively prevents us from having
custom::begin call regardless of existing free functions. How can we do this? Well, we avoid the function call. Like in this famous space movie, these are not the functions you're looking for. Instead of a function
begin, we provide a callable with the name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
In A, we see our callable
begin. It is a plain
struct with a templated call operator. Inside this call-operator, in B, we use
constexpr if from C++17 together with C++20's Concepts ("I love it when a plan comes together" comes to my mind) first to check whether the type
Rng provides a free-function
begin. If so, we call it by moving the data to it. Otherwise, the
else if checks with the same utilities whether
Rng has a member-function
begin. The procedure is the same. If found, the member function is called, and the parameter is moved into it.
Congrats! With this simple change, I hope you agree that it is simple or at least manageable, your code is now more consistent. As long as we call
custom::begin, this function is called first and routes the call to the free or member-function. But there is more.
Chipping in a bit more C++20?
Since we already used abbreviated function templates and Concepts from C++20, why not see what other features from the future that is now here we can apply?
The callable seems a bit much to write. Plus, you all probably know by know that a lambda is a callable as well. In fact, what I presented above could as well be a lambda. The only thing pre-C++20 was that there was no nice way to have a template type-parameter. Yes, C++14's generic lambdas together with
decltype allowed us this already, but isn't the version below cleaner?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
This code here does the same as before. Just that here we use C++20's lambdas with a template-head, allowing us to specify the template type parameter
R. The body of the lambda is a copy of the callable's body.
Next time we continue here and learn how we can avoid dangling pointers, much as ranges do.
I hope you learned something. I appreciate your feedback. Please reach out to me on Twitter or via email.