If you were using Range-v3, you could loop on the views::take(k)
and then actions::drop(k)
afterwards, like this:
#include "fmt/format.h"
#include <iostream>
#include <range/v3/action/drop.hpp>
#include <chrono>
#include <vector>
#include <deque>
#include <ranges>
using ranges::actions::drop;
using std::ranges::views::take;
static constexpr int kBatchSize = 4;
void fn(std::deque<int>& vals) {
fmt::print("\n[");
for (const auto& elem : vals | take(kBatchSize)) {
fmt::print("{} ", elem);
}
fmt::print("]\n");
drop(vals, 4);
}
int main() {
std::deque vals{1,2,3,4,5,6};
fn(vals);
fn(vals);
}
This doesn't really free you from having
an extra iteration
but at least it hides it behind ranges::actions::drop
, which tells very clearly what it does, given its name (drops the elements) and the usage (mutates the input otherwise without smth =
on its left it would be a no-op).
If you want to
- eventually mutate the
vals
parameter by removing some its leading elements,
- but you also want do to something with those elements,
it's clear that you have to do 1 before 2, because those elements get destroyed, so you either
- operate on the elements and delete them one at a time,
- or you operate on all of them and then delete all of them.
The latter approach seems to be more straightforward and it's the one used by the proposed solutions (at least mine and Nicol Bolas'), which means you loop twice on those first elements. The former approach would be more complicated and just loop once instead of twice, which is not a big deal, I think.
Anyway, this is a solution which implements the first approach, looping only once:
#include "fmt/format.h"
#include <range/v3/algorithm/for_each.hpp>
#include <range/v3/view/iota.hpp>
// the other include directives
using namespace ranges;
using namespace ranges::views;
static constexpr int kBatchSize = 4;
void fn(std::deque<int>& vals) {
fmt::print("\n[");
for_each(
iota(0, std::min(kBatchSize, (int)vals.size())),
[&vals](auto){
fmt::print("{} ", vals.front());
vals.pop_front();
}
);
fmt::print("]\n");
}
However, consider that dropping elements one at a time with .pop_front()
can be expensive for other containers (e.g. vector doesn't have it, but if it did, it would very expensive (and that's the reason why it hasn't such a member function)).
Beside this performance consideration, there's also another problem. To highlight it, let's if we can abstract out what you called apply_drop
:
void apply_drop(auto& vals, int n, auto f) {
for_each(
iota(0, std::min(n, (int)vals.size())),
[&vals, f](auto){
f(vals.front());
vals.pop_front();
}
);
}
This does mostly what you meant, I hope. And fn
would be implemented in terms of it like this:
void fn(std::deque<int>& vals) {
fmt::print("\n[");
apply_drop(vals, kBatchSize, [](auto const& x){ fmt::print("{} ", x); });
fmt::print("]\n");
}
But as you see I had to keep a few print
statements in fn
and couldn't bring them in apply_drop
, because doing so would make no sense. Indeed, when in your comment you write
provides a callback customization point.
I think you're referring to a callback function that is called for each element which you pop. And that cuts out the actions fmt::print("\n[");
and fmt::print("]\n");
, which have to be performed one off each, and in different moments; fmt::print("{} ", x);
is the only thing that you are doing once per value, and so that's the only action that can be injected in the function that loops on the values (apply_drop
in our case).
So your fn
is not really simply "removing some leading elments and performing an action on each" (that's what apply_drop
above does), but it's also doing other stuff that's not easy to shoehorn in the into for_each
or another looping abstraction.