C++20 Benefits: Consistency With Ranges

C++20 Benefits: Consistency With Ranges

By Andreas Fertig

Overload, 30(167):18-19, February 2022


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) for Container
  • std::begin for OtherContainer
  • OtherContainer::begin also for OtherContainer

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.

Andreas Fertig 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 (https://cppinsights.io) – enables people to look behind the scenes of C++, and better understand constructs..






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.