Where do you begin when walking over a container in C++? Andreas Fertig shows how C++20 Ranges simplify this.
This article is a short version of Chapter 3, ‘Ranges’, from my latest book Programming with C++20. The book contains a more detailed explanation and more information about this topic.
You have probably all already heard of C++20’s ranges. With ranges-v3, Eric Niebler has already provided us with a solution, independent of C++20 [Niebler]. In this article, I would 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 want to talk about consistency. I assume that you already know about ranges or that you can catch up quickly, so I’m not focussing on the various algorithms ranges bring us, nor the pipe syntax. I want to teach you how ranges help achieve consistency, what this means, and how you can apply it to your own codebase, independently of 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 example in Listing 1.
struct Container {}; // ① Container without begin int* begin(Container); // ② Free-function begin // for Container struct OtherContainer { // ③ Container with begin int* begin(); }; void Use(auto& c) { begin(c); // ④ Call ::begin(Container) std::begin(c); // ⑤ Call STL std::begin } |
Listing 1 |
Essentially, we can see two types there: Container
① and OtherContainer
③. The internals do not matter for this article. What matters is the function begin. We see it in ② as a free-function for Container
and as a member-function in OtherContainer
.
In Use
, we look at an abbreviated function template from C++20. For those who haven’t seen this before, 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. ④ does call a free function begin
, relying on overload-resolution. ⑤, on the other hand, does explicitly call the std
version of begin
.
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 Listing 2.
void Use(auto& c) { using std::begin; // Bring std::begin in the // namespace // Now both functions are in scope begin(c); } |
Listing 2 |
Arthur O’Dwyer wrote a post about two-step with std::swap
, which explains it from a different angle. [O’Dwyer]
The one issue 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:
begin(Container)
forContainer
std::begin
forOtherContainer
OtherContainer::begin
also forOtherContainer
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 could not solve this situation. Here is what you do when ranges are available:
void Use(auto& c) { // Use ranges std::ranges::begin(c); }
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’ve provided our own free function, begin
, for a type, it beats std::begin
. Why? Because this is how ADL works (I’m not going into the details here, it could fill at least another article.)
Just 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 have already learned how you could improve your code with ranges. But you would 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 result as with ranges. We want to have a function, let’s say begin
, which users can customize, but all calls should first go to our begin
function.
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 behaviour as ranges do.
void Use(auto& c) { custom::begin(c); }
The code above is what we need to use. Now let’s see how we build custom::begin
.
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 a custom::begin
call regardless of existing free functions. How can we do this? Well, we avoid the function call. Paraphrased from a famous space movie, ‘These are not the functions you’re looking for.’ Instead of the function begin
, we provide a callable with the name begin
(see Listing 3).
namespace custom { namespace details { struct begin_fn { // ① Callable template<class R> constexpr auto operator()(R&& rng) const { // ② Free-function if constexpr(requires(R rng) { begin(std::forward<R>(rng)); }) { return begin(std::forward<R>(rng)); // Same as above for containers } else if constexpr(requires(R rng) { std::forward<R>(rng).begin(); }) { return std::forward<R>(rng).begin(); } } }; } // namespace details // Callable variable named begin inline constexpr details::begin_fn begin{}; } // namespace custom |
Listing 3 |
In ①, we see our callable begin
. It is a plain struct
with a templated call operator. Inside this call-operator, in ②, we use constexpr if
from C++17 together with C++20’s Concepts (“I love it when a plan comes together” comes to 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 have already used abbreviated function templates and Concepts from C++20, why not see what other features from the future are here now that we can apply?
The callable seems a bit much to write. Plus, you all probably know by now 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 to do this already, but isn’t the version below cleaner? (See Listing 4.)
namespace custom { namespace details { constexpr auto begin_fn = []<class R>(R&& rng) { // Callable // Free-function if constexpr(requires(R rng) { begin(std::forward<R>(rng)); }) { return begin(std::forward<R>(rng)); // Same as above for containers } else if constexpr(requires(R rng) { std::forward<R>(rng).begin(); }) { return std::forward<R>(rng).begin(); } }; } // namespace details // Callable variable named begin inline constexpr auto begin = details::begin_fn; } // namespace custom |
Listing 4 |
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.
References
[Niebler] range-v3, available on GitHub: https://github.com/ericniebler/range-v3
[O’Dwyer] Arthur O’Dwyer, ‘What is the std::swap two-step?’, available at https://quuxplusone.github.io/blog/2020/07/11/the-std-swap-two-step/
This article was first published on Andreas Fertig’s blog (https://andreasfertig.blog/2021/05/cpp20-benefits-consistency-with-ranges/) on 4 May 2021.
https://cppinsights.io) – enables people to look behind the scenes of C++, and better understand constructs..
is a trainer and lecturer on C++11 to C++20, who presents at international conferences. Involved in the C++ standardization committee, he has published articles (for example, in iX) and several textbooks, most recently Programming with C++20. His tool – C++ Insights (