4

I find the traditional syntax of most c++ stl algorithms annoying; that using them is lengthy to write is only a small issue, but that they always need to operate on existing objects limits their composability considerably.

I was happy to see the advent of ranges in the stl; however, as of C++20, there are severe shortcomings: the support for this among different implementations of the standard library varies, and many things present in range-v3 did not make it into C++20, such as (to my great surprise), converting a view into a vector (which, for me, renders this all a bit useless if I cannot store the results of a computation in a vector).

On the other hand, using range-v3 also seems not ideal to me: it is poorly documented (and I don't agree that all things in there are self-explanatory), and, more severely, C++20-ideas of ranges differ from what range-v3 does, so I cannot just say, okay, let's stick with range-v3; that will become standard anyway at some time.

So, should I even use any of the two? Or is this all just not worth it, and by relying on std ranges or range-v3, making my code too difficult to maintain and port?

Bubaya
  • 615
  • 3
  • 13
  • 1
    You might want to wait for c++23 or later. New concepts take a while to become standard. Or just use what is already there and accept that not everything works yet. – Goswin von Brederlow Jul 01 '22 at 10:17
  • 1
    It's chicken-and-egg: if you don't use ranges, so you don't write things that take ranges, so you don't use ranges. Its also not that hard to write `template Container range_to(Range&&);` – Caleth Jul 01 '22 at 11:35
  • Or even a view that implicitly converts to any container constructible from the `begin(), end()` of the viewed range. – Caleth Jul 01 '22 at 11:39
  • 1
    C++20 features are still under great development by compilers. It is too soon to use this in many projects since progress of implementation of this features varies between compilers. Ranges is one of the areas which are still under construction/testing and so on. – Marek R Jul 01 '22 at 11:42
  • 1
    The Standard Library can be split in two parts: The essential part (which you can't write in portable C++) and the convenient part (which you could write in portable C++, but you don't have to since it's already included). While C++23 will add more support for Ranges, it appears that's entirely convenience. If you needs bit today, you can implement them now and rely on a `using std::ranges::Foo` in 2024. – MSalters Jul 01 '22 at 11:56
  • 3
    C++ algorithms **do not** need to operate on existing objects. That's what makes them composable: the iterator returned by one algorithm can serve as the input to another. Often the iterators that are passed to an algorithm come from a container, but that is not a requirement; there are other sources of iterators. – Pete Becker Jul 01 '22 at 13:10
  • 1
    @MSalters: "*If you needs bit today, you can implement them now*" No, you can't. You cannot inject (non-specialization) declarations into the `std` namespace. And several C++23 features cannot work without changing the existing infrastructure. You can implement a couple of bolted-on features like the "view to container" bit, but that's about it. – Nicol Bolas Jul 01 '22 at 13:35
  • @PeteBecker OK, not in general, you're right. But for `transform`, which I use a lot, it's difficult, as `transform` requires an output iterator. – Bubaya Jul 01 '22 at 14:09
  • @Bubaya -- forward iterators, bidirectional iterators, and random access iterators all meet the requirements for output iterators. And there's `std::insert_iterator` that serves as an output iterator and inserts into a container. Do you have some specific issue that you're concerned with, or just sweeping generalizations? – Pete Becker Jul 01 '22 at 17:26
  • 1
    @PeteBecker I should open a new question for the specific questions on how to put the stl algorithms together. – Bubaya Jul 01 '22 at 19:58

5 Answers5

6

Is using ranges in c++ advisable at all?

Yes.

and many things present in range-v3 did not make it into C++20, such as (to my great surprise), converting a view into a vector

Yes. But std::ranges::to has been adopted by C++23, which is more powerful and works well with C++23's range version constructor of stl containers.

So, should I even use any of the two?

You should use the standard library <ranges>.

It contains several PR enhancements such as owning_view, redesigned split_view, and ongoing LWG fixes. In addition, C++23 brings not only more adapters such as join_with_view and zip_view, etc., but also more powerful features such as pipe support for user-defined range adaptors (P2387), and formatting ranges (P2286), etc. The only thing you have to do is wait for the compiler to implement it. You can refer to cppreference for the newest compiler support.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • 3
    Advisable on what grounds, please? _Yes_ is woefully uninformative, is it? – Maxim Egorushkin Oct 01 '22 at 19:49
  • "The only thing you have to do is wait for the compiler to implement it". That's a rather big thing, is it not? I assume the OP is asking about programming needs they have today. – user118967 Oct 15 '22 at 21:27
4

I recommend using range-v3 and not std::ranges. There are too many things missing (at least before c++23 is implemented) to make it worth using std::ranges at all.

On the other hand, using range-v3 also seems not ideal to me: it is poorly documented (and I don't agree that all things in there are self-explanatory),

It's easily enough to learn range-v3 from these supplementary materials https://www.walletfox.com/course/quickref_range_v3.php https://www.walletfox.com/course/examples_range_v3.php and you could always buy the book if you want more.

Also range-v3 is open source so you can let the source code be your documentation.

and, more severely, C++20-ideas of ranges differ from what range-v3 does, so I cannot just say, okay, let's stick with range-v3; that will become standard anyway at some time.

I doubt these changes will matter, much, the main problem is that range-v3 and std::ranges dont combine but changing the namespaces should be most of the effort porting range-v3 to std::ranges 23.

making my code too difficult to maintain

Code without ranges is too difficult. The amount of time I save by using range-v3 for everything is enormous, particularly the time taken ironing out the bugs in freshly written code, but also the time it takes to understand code you've written in the past, and then modify it. I think the only reason to not use range-v3 is to maintain the conventions of an existing codebase.

Tom Huntington
  • 2,260
  • 10
  • 20
4

As a ranges addict, I'm going answer again this time in the negative.

Most of time you spend developing, is spent incrementally compiling one compilation unit. Using ranges drastically increases these compile times. msvc compiles significantly faster and when I switch gcc or clang, it's unbearable.

You cant solve this by setting up compilation walls, since you pretty much always have to deduce the type of your ranges. So you are mostly stuck with slow compile times even when you're not modifying ranges code.

Getting the templates to compile is also a waste of time. After using Python's iterables you really start noticing the arbitrary limitations of the static type system. There are a lot of quirks you have to learn the hard way about.

C++ ranges are quite complicated. I'm trying to be less nerdy, and if you are too, staying away might be a good idea.

The declarative code is far more readable and maintainable than imperative. Functional programming pushes all the error prone detailed orientated code out of your code and into the library. But at what cost? map, reduce, filter are all easy enough to implement imperatively, but I need my group_by and split.

Tom Huntington
  • 2,260
  • 10
  • 20
  • 1
    I'm confused by the juxtaposition-without-comment of https://stackoverflow.com/a/72846230 and https://stackoverflow.com/a/74664658 . In July 2022 you said confidently "Code without ranges is too difficult. The amount of time I save ... is enormous." Five months later, you said confidently "Using ranges drastically increases these compile times ... Getting the templates to compile is also a waste of time ... a lot of quirks ... quite complicated ... staying away is recommended." Did you really flip 180º in only five months? How many months had you been using Ranges before July? – Quuxplusone Aug 13 '23 at 17:32
  • If your computer is slow enough, everything can be too slow. Which kind of system do you have? Is ranges too heavy for every user or only some? – Daniel Aug 13 '23 at 18:31
  • @Daniel it really depends on how much you deep you are composing the ranges. Every pipe operator is a nested template instantiation. Inferring the lambda type also can slow things down. I'm running an `i5-4590 CPU @ 3.30GHz, 3301 Mhz, 4 Core(s), 4 Logical Processor(s)` – Tom Huntington Aug 14 '23 at 05:04
  • @Quuxplusone Writing declarative code speeds up development time (imperative code can be a nightmare to maintain/modify). Using ranges slows down compilation time. The `staying away` was qualified. I have a big project in c++ in which ranges provide a big benefit, but it comes with I high cost – Tom Huntington Aug 14 '23 at 05:09
1

Simple example : sort vector of one hundred million random int values

#include <iostream>
#include <chrono>
#include <ranges>
#include <random>
#include <vector>
#include <algorithm>


int main(int argc, char **argv) {


    const int START = 1, END = 50, QUANTITY = 100000000;


    std::random_device dev;
    std::mt19937 rng(dev());
    std::uniform_int_distribution<std::mt19937::result_type> dist6(START, END);

    std::vector<int> vec;
    vec.reserve(QUANTITY);

    for (int i = 0; i < QUANTITY; i++) {
        vec.push_back(dist6(rng));
    }

    std::vector<int> original_copy = vec;

    auto start_test1 = std::chrono::high_resolution_clock::now();
    std::ranges::sort(vec);
    auto end_test1 = std::chrono::high_resolution_clock::now();
    auto duration_test1 = std::chrono::duration_cast<std::chrono::milliseconds>(end_test1 - start_test1).count();

    auto start_test2 = std::chrono::high_resolution_clock::now();
    std::sort(original_copy.begin(), original_copy.end());
    auto end_test2 = std::chrono::high_resolution_clock::now();
    auto duration_test2 = std::chrono::duration_cast<std::chrono::milliseconds>(end_test2 - start_test2).count();


    std::cout << "test std::ranges::sort, vector was sorted in  " << duration_test1 << " milliseconds." << std::endl;
    std::cout << "test std::sort, vector was sorted in  " << duration_test2 << " milliseconds." << std::endl;


    if (duration_test1 > duration_test2) {
        std::cout << "std::sort is " << duration_test1 - duration_test2 << " milliseconds faster" << std::endl;
    } else {
        std::cout << "std::ranges::sort is " << duration_test2 - duration_test1 << " milliseconds faster" << std::endl;
    }


    return 0;
}

output :

test std::ranges::sort, vector was sorted in  175319 milliseconds.
test std::sort, vector was sorted in  45368 milliseconds.
std::sort is 129951 milliseconds faster

in my opinion there is something strange in std::ranges, maybe it is easy to use than standard algorithms, but performance could be better

andriy-byte
  • 21
  • 1
  • 4
  • 1
    You missed warming up CPU cache, ensuring CPU frequency is constant and eliminating context switches. Not sure what you measured exactly, there is no interpretation/explanation of the difference. – Maxim Egorushkin Oct 01 '22 at 19:52
  • 1
    https://github.com/google/benchmark would be a better start. – Maxim Egorushkin Oct 01 '22 at 19:56
  • 1
    Quality of implementations may [vary](https://quick-bench.com/q/3AWmwFCpQTFqx4xRASNKNl6qNz8), but, besides all the things mentioned in the previous comment, have you enabled optimizations while compiling? – Bob__ Oct 01 '22 at 20:06
  • I compiled the above code under Clang with MSVC STL and got the following result: ```test std::ranges::sort, vector was sorted in 1617 milliseconds. test std::sort, vector was sorted in 1612 milliseconds. std::sort is 5 milliseconds faster``` Based on the above times being almost 10x bigger I think those are Debug times. – Chris Dec 02 '22 at 12:10
0

Your title doesn't match your question

Your title asks "Is it advisable to use Ranges at all?" but in your question you indicate that you're considering using range-v3 — should you use range-v3 or C++20 Ranges?

That's like asking "Is it advisable to use ASIO at all?" and then indicating that you're trying to choose between Boost.ASIO and standalone ASIO. If one is trying to choose between those options, one's clearly already decided to "use ASIO at all," hasn't one? So in your case, you seem to have already decided to "use Ranges at all," and now we're just haggling over the price.

My answer below is therefore aimed probably not at you, but at the hypothetical reader who's wondering whether to introduce C++20 Ranges into a codebase that isn't already fundamentally built around Ranges.

No and yes; or, "You can't avoid Ranges."

It really depends on what you're going to be using Ranges for. IMO it is fairly evident by now that "range-view-ifying" ordinary business-logic code is a bad idea, both for understandability and performance. For example, please don't change

for (int i : selected_indices) {
    if (products[i].price > 10) {
        std::cout << products[i].name;
    }
}

into

std::ranges::copy(
    selected_indices
        | std::views::filter([](auto& p) { return p.price > 10; })
        | std::transform(&Product::name),
    std::ostream_iterator<std::string_view>(std::cout)
);

However, it is perfectly reasonable to change

int expected[] = {1,2,3,4,5};
EXPECT_TRUE(std::equal(actual.begin(), actual.end(), expected, expected+5));

into

int expected[] = {1,2,3,4,5};
EXPECT_TRUE(std::ranges::equal(actual, expected);

That's also "C++20 Ranges code." But this time it's actually improving the readability of the code. (It's still increasing the compile-time cost, and leaves the runtime cost unchanged.)

Also, if you are already writing C++98-STL-style "algorithms" via generic programming, you should definitely adopt C++20's modifications to the iterator model, so that your algorithms will work both with old-style iterators and with new-style iterators. That is, I think it could be worthwhile to rewrite a utility library of the form

template<class It, class Pred>
bool my::is_uniqued(It first, It last, Pred pred) {
  for (auto it = first; it != last; ++it) {
    if (pred(*first, *std::next(first))) {
      return false;
    }
  }
  return true;
}

into something more C++20-friendly like

template<class It, class Sent, class Pred>
bool my::is_uniqued(It first, Sent last, Pred pred) {
  for (auto it = first; it != last; ++it) {
    if (pred(*first, *std::next(first))) {
      return false;
    }
  }
  return true;
}

template<std::ranges::range R>
bool my::is_uniqued(R&& rg) {
  return my::is_uniqued(rg.begin(), rg.end());
}

This is kind of like when you update a const string&-taking function to take string_view instead, thus permitting it to take more kinds of string-like arguments. We're updating the is_uniqued function to take more kinds of iterable-range-like arguments. This could be seen as a benefit to "code hygiene."

In this example I can't think of any particular reason to start using std::ranges::next in place of std::next; and you probably shouldn't add constraints like

template<std::forward_iterator It, std::sentinel_for<It> Sent, std::predicate<std::iter_reference_t<It>> Pred>
bool my::is_uniqued(It first, Sent last, Pred pred) {

because that'll just trash your compile times, and your compiler-diagnostic spew when something goes wrong. It also runs the risk of making your template unusable for some existing hand-coded C++98 iterator type that fails to satisfy some detail of std::forward_iterator. (The most common way for this to happen, in my experience, is that someone forgot to const-qualify its operator*(). I would consider such an iterator type defective, and worth fixing, but you might not have the time or permission to go fix it right away.) It also opens the bikeshed door: "Why did you write std::iter_reference_t instead of std::iter_value_t? Should we maybe constrain on both?" Having a blanket style rule that "we don't unnecessarily constrain our templates" can shortcut a lot of bikeshedding.

On the other hand, if you're producing a library that currently hand-codes a lot of enable_ifs specifically to emulate what Ranges does natively... well, of course it will be beneficial to use C++20 Ranges instead of that!

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159