Bubaya
Bubaya

Reputation: 823

Is using ranges in c++ advisable at all?

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?

Upvotes: 11

Views: 8805

Answers (5)

Tom Huntington
Tom Huntington

Reputation: 3415

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.

Upvotes: 6

Quuxplusone
Quuxplusone

Reputation: 27045

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!

Upvotes: 1

andriy-byte
andriy-byte

Reputation: 33

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

Upvotes: -3

Tom Huntington
Tom Huntington

Reputation: 3415

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.

Upvotes: 4

康桓瑋
康桓瑋

Reputation: 42736

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.

Upvotes: 11

Related Questions