What is the best way to drop last element using c++20 ranges

Viewed 364

Is there any better way to drop last element in container using c++20 ranges than reverse it twice?

#include <iostream>
#include <vector>
#include <ranges>

int main()
{
    std::vector<int> foo{1, 2, 3, 4, 5, 6};

    for (const auto& d: foo | std::ranges::views::reverse 
                            | std::ranges::views::drop(1) 
                            | std::ranges::views::reverse)
    {
        std::cout << d << std::endl;
    }
}
3 Answers

What you need is views::drop_last which comes from p2214 and has a priority of Tier 2.

As the paper says:

We’ll go through the other potential range adapters in this family and discuss how they could be implemented in terms of existing adapters:

  • take_last(N) and drop_last(N). views::take_last(N) is equivalent to views::reverse | views::take(N) | views::reverse. But this is somewhat expensive, especially for non-common views. For random-access, sized ranges, we’re probably want r | views::take_last(N) to evaluate as r | views::drop(r.size() - N), and that desire is really the crux of this whole question — is the equivalent version good enough or should we want to do it right?

Since vector is a random-access, sized range, you can just do

for (const auto& d: foo | std::views::take(foo.size() - 1))
{
    std::cout << d << std::endl;
}

How about using span?

#include <iostream>
#include <span>
#include <vector>

int main() {
  std::vector<int> foo{1, 2, 3, 4, 5, 6};

  for (const auto& d : std::span(foo.begin(), foo.end() - 1)) {
    std::cout << d << '\n';
  }
}

You can do a good approximation via:

struct drop_last_t {
    template <std::ranges::sized_range R>
        requires std::ranges::viewable_range<R>
    friend auto operator|(R&& r, drop_last_t) {
        auto n = std::ranges::size(r);
        return std::views::take(std::forward<R>(r), n > 0 ? n - 1 : 0);
    }
};
inline constexpr drop_last_t drop_last;

That lets you:

for (const auto& d: foo | drop_last)

This isn't a perfect range adaptor, since you can't write something like auto my_adaptor = transform(f) | drop_last; In order to do that, you need P2387, which is a C++23 library feature. In C++23, you'd write it this way:

struct drop_last_t : std::ranges::range_adaptor_closure<drop_last_t>
{
    template <std::ranges::sized_range R>
        requires std::ranges::viewable_range<R>
    auto operator()(R&& r) const {
        auto n = std::ranges::size(r);
        return std::views::take(std::forward<R>(r), n > 0 ? n - 1 : 0);
    }
};
inline constexpr drop_last_t drop_last;    

And now this is a completely functional range adaptor. The current libstdc++-specific version looks like this (just to demonstrate, don't actually do this - this isn't how you'd do this in C++23).


Of course, this is limited to sized ranges. There's all sorts of directions this could go. You could support any forward range by doing std::ranges::distance(r) instead (at the cost of multiple traversal). But a bespoke implementation could do better. For bidi+common, you just need to stop at prev(end(r)). For forward only, you could advance two iterators at a time, etc. Just something to think about.

Related