C++20 introduced Ranges to the standard library: a new way of expressing composable transformations on collections of data. This feature adds a huge amount of expressive power and flexibility to C++. As with many concepts in C++, that power comes with new concepts to learn, and some complexity which can be difficult to navigate. One way of taming that complexity is through complete, clear, comprehensive documentation. Christopher Di Bella and Sy Brand (one of the co-authors of this post) presented their ideas for C++ documentation in the era of concepts in their CppCon 2021 talk. Tyler Whitney (the other co-author, and manager of our C++ documentation at Microsoft) has expanded these ideas and exhaustively documented the range adaptors available in the standard library for you. To check it out, go to on Microsoft Learn. In this post, we’ll talk through some of the complexity of using and documenting Ranges, and outline the principles behind the documentation which Tyler has written. But first, a quick introduction to Ranges for those of you who are not familiar with the feature. What are Ranges? At a high level, a range is something that you can iterate over. A range is represented by an iterator that marks the beginning of the range, and a sentinel that marks the end of the range. The sentinel may be the same type as the begin iterator, or it may be different, which lets Ranges support operations which simple iterator pairs can’t. The C++ Standard Library containers such as vector and list are ranges. A range abstracts iterators in a way that simplifies and amplifies your ability to use the Standard Template Library (STL). STL algorithms usually take iterators that point to the portion of the collection that they should operate on. For example, you could sort a vector with std::sort(myVector.begin(), myVector.end());. However, carrying out an algorithm across a range is such a common operation, we’d rather not have to retrieve the begin and end iterators every time. With ranges, you can instead call std::ranges::sort(myVector);. But perhaps the most important benefit of ranges is that you can compose STL algorithms that operate on ranges in a style that’s reminiscent of functional programming. Traditional C++ algorithms don’t compose well. For example, if you wanted to build a vector of squares from the elements in another vector that are divisible by three, you could write something like: std::vector input = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; std::vector intermediate, output; std::copy_if(input.begin(), input.end(), std::back_inserter(intermediate), [](const int i) { return i%3 == 0; }); std::transform(intermediate.begin(), intermediate.end(), std::back_inserter(output), [](const int i) {return i*i; }); With ranges, you can produce a range with the same values without the intermediate vector: // requires /std:c++20 std::vector input = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto output = input | std::views::filter([](const int n) {return n % 3 == 0; }) | std::views::transform([](const int n) {return n * n; }); Besides being easier to read, this code avoids the memory allocation that’s required for the intermediate vector and its contents. It also allows you to compose two operations. In the preceding code, each element that’s divisible by three is combined with an operation to square that element. The pipe (|) symbol chains the operations together and is read […]