I'm afraid just about everybody who read this question has misunderstood it. I'm not trying to find the best way of printing a vector. I'm trying to find the best way of excluding part of an operation from the last iteration in a loop. I'm seeking advice about a pattern that is repeated is many different contexts. For this reason, I cannot provide a concrete example.
This question is off-topic
A well-formed comma-separated list has commas after all but the last element. This means that when dealing with these lists, the comma has to be dealt with on all but the last iteration. This is how I do things currently:
for (auto e = vec.cbegin(); e != vec.cend(); ++e) {
// code that has to be executed for every element
std::cout << *e;
if (e != vec.cend() - 1) {
// code that must not be executed for the last element
std::cout << ", ";
}
}
There are five instances of the above pattern in the project that I'm working on. I feel like there might be a more elegant way of doing this. An alternative that I've come up with is this:
// infinite loop if vec is empty
if (!vec.empty()) {
for (auto e = vec.cbegin(); e != vec.cend() - 1; ++e) {
// code that has to be executed for every element
std::cout << *e;
// code that must not be executed for the last element
std::cout << ", ";
}
// code that has to be executed for every element
std::cout << vec.back();
}
This alternative is slightly faster because there isn't an if
in the body but that's not a big deal. The if
is true for all but the last iteration so the branch predictor will probably only miss on the last iteration. I also have to repeat code that has to be executed for every element
which means that I need to hoist that into a function.
The first snippet is the best that I can come up with but I feel like this could be done better. Note that writing to std::cout
is just an example.
6 Answers 6
If you are just doing a single-shot printing of a container, just print the first element and then print delim-value pairs. This reduces the iterator requirements to being equality comparable, dereferenceable, and incrementable.
if (!vec.empty()) { // invert to return immediately if this is a function
auto first = vec.cbegin();
std::cout << *first;
while (vec.cend() != ++first) {
std::cout << ", " << *first;
}
}
There is also a stateful approach that reduces the operation strength from branching on whether to print the delimiter to a pointer assignment.
auto actual_delim = ", ";
auto delim = "";
for (const auto& elem : vec) {
std::cout << delim << elem;
delim = actual_delim;
}
See infix_iterator
where this stateful approach is used.
-
\$\begingroup\$ As mentioned in my answer, this approach has the distinct advantage of duplicating the logic of the loop body. \$\endgroup\$Konrad Rudolph– Konrad Rudolph2018年11月06日 13:53:13 +00:00Commented Nov 6, 2018 at 13:53
-
\$\begingroup\$ Second version is much more appealing, as we don't lose range-based
for
(and you can set a function pointer or plainbool
variable instead of a string-pointer if necessary). In a worst case, we could test&elem == &vec.back()
if we can't make the first element have the variant behaviour (e.g. joining a list with" and "
as the final separator). \$\endgroup\$Toby Speight– Toby Speight2018年11月06日 17:39:33 +00:00Commented Nov 6, 2018 at 17:39
I'm not sure that elegance should be your first concern here, because the algorithm should be very simple and the code short however you write them.
I/O operations will have a much greater impact on performance than the lay-out of your loop, so you shouldn't worry too much about speed either.
Generality, I believe, is paramount in this case. That's why I see vec.cend() - 1
as the main short-coming of your proposal, because it restricts your algorithm to at least bidirectional iterators, when your goal can be easily achieved for any kind of iterator.
Here's what I would consider better:
#include <iostream>
#include <vector>
template <typename Container, typename Separator> // any container is acceptable...
void pretty_print(const Container& c, const Separator& s) {
std::cout << '{';
auto it = std::begin(c); // ... even arrays
if (it != std::end(c)) do {
std::cout << *it;
if (++it == std::end(c)) break; // that way you don't need to decrement the iterator
std::cout << s;
} while (true);
std::cout << '}';
}
-
\$\begingroup\$ I did mention that writing to
std::cout
was just an example but I do like that this works with forward iterators. \$\endgroup\$Indiana Kernick– Indiana Kernick2018年11月06日 08:57:17 +00:00Commented Nov 6, 2018 at 8:57 -
\$\begingroup\$ I think I have to agree with Calak (answer below): this variant is decidedly less readable than others. But it would help to make it equivalent to what OP is doing to have a direct comparison (i.e. remove the printing of the braces). \$\endgroup\$Konrad Rudolph– Konrad Rudolph2018年11月06日 10:08:41 +00:00Commented Nov 6, 2018 at 10:08
I always just use an additional variable:
auto first= true;
for (auto const& x : list) {
if (first) first = false; else separator(x);
action(x);
}
In your case, separator(x)
would be std::cout << ", "
(x
isn’t actually used), and action(x)
would be std::cout << x
.
I normally abhor mutating variables inside a loop and try to use standard library algorithms on lambdas without side-effects instead. But in this case I believe having an additional boolean flag is simply the most economical, most readable solution.
This is also one of the very few cases (the only?) where I’m using a single-line if
...else
.
The advantage of this method is that you don’t have to duplicate action(x)
. Even if it’s just a single, simple expression, having such a duplication makes the logic disjointed and harder to follow.
The disadvantage is that the condition first
has to be checked on every loop iteration. My suspicion is that the CPU’s branch predictor will handle this extremely well so it shouldn’t impact performance. Still, it’s conceptually irksome.
-
\$\begingroup\$ As always I’d love an explanation for downvotes. \$\endgroup\$Konrad Rudolph– Konrad Rudolph2018年11月06日 13:50:18 +00:00Commented Nov 6, 2018 at 13:50
-
\$\begingroup\$ I upvoted this answer because it is succinct, readable, generalisable, and has no side-effects when skipping
separator(x)
(in the example, to std::cout << ""; In my use-case separator(x) was sleep, which may yield even if the time argument is 0.). \$\endgroup\$Omer Anson– Omer Anson2024年06月29日 18:38:45 +00:00Commented Jun 29, 2024 at 18:38
I would use an infix iterator:
Example here: https://stackoverflow.com/q/3496982/14065
std::copy(vec.begin(), vec.end(), infix_iterator(std::cout, ", "));
Or if you ahve the latest version of the compiler:
https://en.cppreference.com/w/cpp/experimental/ostream_joiner
std::copy(vec.begin(), vec.end(), std::ostream_joiner(std::cout, ", "));
-
\$\begingroup\$ Please read the first paragraph of the question \$\endgroup\$Indiana Kernick– Indiana Kernick2018年11月06日 21:50:14 +00:00Commented Nov 6, 2018 at 21:50
-
1\$\begingroup\$ @Kerndog73: I did. Does not change my answer. Still the best way to do it. Of course you need to customize the iterator for your situation but the same principles still applies. You are applying a standard action to each element and an extra action to all but the last element. \$\endgroup\$Loki Astari– Loki Astari2018年11月06日 21:55:28 +00:00Commented Nov 6, 2018 at 21:55
-
\$\begingroup\$ So you're suggesting I make a special iterator that accepts a couple of lambdas and iterates a container? \$\endgroup\$Indiana Kernick– Indiana Kernick2018年11月06日 21:57:56 +00:00Commented Nov 6, 2018 at 21:57
-
\$\begingroup\$ @Kerndog73: You are applying an action to all elements. Then you are applying another action to all but the last element. Sounds like an algorithmic processes that is covered by the above. \$\endgroup\$Loki Astari– Loki Astari2018年11月06日 22:00:00 +00:00Commented Nov 6, 2018 at 22:00
-
\$\begingroup\$ After all that has been said by all the answers. I think I'm just going to keep doing things the way that I'm doing them. \$\endgroup\$Indiana Kernick– Indiana Kernick2018年11月06日 22:03:16 +00:00Commented Nov 6, 2018 at 22:03
We are getting closer to the off-topic "opinion-based" border
What's readable is a matter of taste. Honestly, i find the @papafaga version less fluent. You also have the possibility to do the opposite of your second version, to put the particular case before the loop, I do it sometimes when the case is trivial.
template <typename Container, typename Separator>
void pretty_print4(const Container& c, const Separator& s) {
if (c.empty()) return;
auto it = std::begin(c);
std::cout << *it;
std::advance(it,1);
while (it != std::end(c)) {
std::cout << s;
std::cout << *it;
std::advance(it,1);
}
}
If you talk about performance, I think all versions are worth. Without optimizations, some versions generate more instructions than others, but once in -O3
, except one or two instructions, it's almost the same. After, if it really matters, you have to look at the machine code and benchmark.
-
\$\begingroup\$ Why use
std::advance
to simply increment by 1? Any iterator hasopertor++
. \$\endgroup\$Ruslan– Ruslan2018年11月06日 21:04:05 +00:00Commented Nov 6, 2018 at 21:04 -
-
3\$\begingroup\$ @Calak The reasoning in your link only makes sense for increment values ≠ 1. And
std::advance
is not more explicit than using++
. The latter is precisely defined and forms part of the iterator contract. It couldn’t be more explicit. \$\endgroup\$Konrad Rudolph– Konrad Rudolph2018年11月06日 23:02:45 +00:00Commented Nov 6, 2018 at 23:02
I would treat the work to do as [head] followed by [tail]. The head item is not preceded by a separator while all tail items are preceded by a separator. The code can use this difference. The pseudocode then looks something like:
// Process head.
processHeadItem()
// Process tail
repeat
processSeparator()
processNextTailItem()
until (tail all processed)
In real code you could start the tail processing loop with for (int i = 1, ...
since i = 0
is the head item.
-
\$\begingroup\$ This was already suggested by Calak and Snowhawk \$\endgroup\$Indiana Kernick– Indiana Kernick2018年11月06日 22:09:10 +00:00Commented Nov 6, 2018 at 22:09
-
\$\begingroup\$ Yes, but I thought my presentation was clearer, more general and gave some of the theory behind the solution: [Head], [Tail]. \$\endgroup\$rossum– rossum2018年11月06日 22:43:27 +00:00Commented Nov 6, 2018 at 22:43
ostream_joiner
- is this reinventing-the-wheel, or do you need something for straight C++17 withoutstd::experimental
? \$\endgroup\$main()
to exercise a function you've made for your project, for example, and you should definitely include the headers and/or definitions you use. I want this to be a good, on-topic question, and it's frustrating that it's not (yet). \$\endgroup\$