Show Lecture.Iterators as a slide show.
CS253 Iterators
Inclusion
Use of advance() or next() requires:
#include <iterator>
The free functions begin(), end(), and size() are defined in all
header files that define a container, such as:
#include <array>
#include <deque>
#include <forward_list>
#include <iterator>
#include <list>
#include <map>
#include <set>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
Foundation
- Before we talk about iterators, let’s review just what
a class can do.
- We all know a class can provide public methods & data.
- It can also provide types.
class Foo {
public:
auto bar() const { return "Yow!\n"; }
int x = 42;
using cash = float;
};
Foo::cash salary = 2.43;
Foo f;
cout << salary << ' ' << f.x << ' ' << f.bar();
2.43 42 Yow!
Array Traversal
You have an array, and you want to traverse (walk through) it.
int a[] = {2, 3, 5, 7, 11, 13, 17, 19};
for (int i=0; i!=8; ++i)
cout << a[i] << ' ';
2 3 5 7 11 13 17 19
a[i]
is the same as *(a+i)
.
- It requires addition, scaling, and indirection.
- Though they’re often combined in a single machine instruction.
- I usually say
i<8
, rather than i!=8
.
- Similarly, I usually say
i++
rather than ++i
.
- Patience.
Problems with indexing
- We’re really used to traversing an array like that.
- We’re so much used to it, that it seems the right & natural way.
- After all, mathematicians do that with
ai, ai+1, ai+2, …
- Trouble is, it doesn’t correspond to how we actually store data.
We can speak of the n th element of a
linked list or a binary tree, but we don’t actually use them that way.
- Instead, we think in terms of pointers.
- We naturally
use pointers to walk a linked list, or to traverse a binary tree.
Array Traversal via Pointers
int a[] = {2, 3, 5, 7, 11, 13, 17, 19};
for (int *p = &a[0]; p != &a[8]; ++p)
cout << *p << ' ';
2 3 5 7 11 13 17 19
- This uses pointers.
- Instead of
int i
, we use int *p
. It points to each element,
one-by-one.
p
initially points to &a[0]
, the address of the first element.
a
contains eight elements, a[0]
…a[7]
. Therefore, when
we get to the address &a[8]
, it’s time to stop.
- This might be a bit faster: instead of
a[i]
(addition, scaling, indirection) we have just indirection.
- Most modern CPUs have addressing modes that can do that
all in one instruction, however.
You can traverse a vector in the same way, since the vector
elements are contiguous:
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (int *p = &a[0]; p != &a[8]; ++p)
cout << *p << ' ';
2 3 5 7 11 13 17 19
or, avoiding the magic number 8:
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (int *p = &a[0]; p != &a[a.size()]; ++p)
cout << *p << ' ';
2 3 5 7 11 13 17 19
Types and Methods
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (vector<int>::iterator it = a.begin(); it != a.end(); ++it)
cout << *it << ' ';
2 3 5 7 11 13 17 19
iterator
is a type provided by the templated vector class.
- You can treat it like a pointer.
- You don’t care what type it really is. You know that:
*
and ++
work on it
- you can assign
.begin()
to it
- you can compare it to
.end()
- What type is it?
- might be int *, might not (it’s unspecified!)
auto is your friend
This is prettier:
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (auto it = a.begin(); it != a.end(); ++it)
cout << *it << ' ';
2 3 5 7 11 13 17 19
All that we did is we used auto to tell the compiler to
make it
the same type as a.begin()
returns,
namely, vector<int>::iterator
.
This is (nearly) exactly the same, and gorgeous :
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (auto v : a)
cout << v << ' ';
2 3 5 7 11 13 17 19
The for loop is defined to use .begin()
and .end()
,
just as the previous code.
It’s simple
Really, the for-loop like that (sometimes called a for…each loop)
is just a mechanical source-level transformation. It rewrites this:
set<string> s = {"my", "dog", "has", "fleas"};
for (auto v : s)
cout << v << '\n';
dog
fleas
has
my
as this:
set<string> s = {"my", "dog", "has", "fleas"};
for (auto it = s.begin(); it != s.end(); ++it) {
auto v = *it;
cout << v << '\n';
}
dog
fleas
has
my
Really
See this error message:
double zulu;
for (auto v : zulu) // 🦡
cout << v;
c.cc:2: error: ‘begin’ was not declared in this scope
“begin”? Why is it complaining about “begin”? Because it
rewrote that for loop to use .begin()
and .end()
.
Variations
Which is best? Why?
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (int v : a)
cout << v << ' ';
2 3 5 7 11 13 17 19
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (const auto v : a)
cout << v << ' ';
2 3 5 7 11 13 17 19
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (auto &v : a)
cout << v << ' ';
2 3 5 7 11 13 17 19
vector<int> a = {2, 3, 5, 7, 11, 13, 17, 19};
for (const auto &v : a)
cout << v << ' ';
2 3 5 7 11 13 17 19
Other containers
The same iterator code works for all STL containers.
forward_list<char> l = {'a', 'c', 'k', 'J'};
for (auto it = l.begin(); it != l.end(); ++it)
cout << *it << ' ';
a c k J
unordered_set<string> u = {"a", "c", "k", "J"};
for (auto it = u.begin(); it != u.end(); ++it)
cout << *it << ' ';
J k c a
set<char> s = {'a', 'c', 'k', 'J'};
for (auto it = s.begin(); it != s.end(); ++it)
cout << *it << ' ';
J a c k
“Jack” is ASCII-sorted.
Of the top 100 ♀ + top 100 ♂ U.S. names, 1918–2017, the ones in ASCII
order: Amy, Ann, Betty, Billy, Gary, Harry, Henry, Jack, Jerry, Kelly,
Larry, Mary, Roy, Scott, and Terry.
iterator
type
- What type is
set<int>::iterator
?
- Not an int *, for sure!
- User-defined type with an internal node pointer,
with
operator*
and operator++
defined.
- Similarly for
list<int>::iterator
and unordered_set<int>::iterator
.
- Many iterator types can’t be native pointers.
- What happens when you increment a plain pointer?
Go to the next memory location.
- Is that what you want for a linked list, a tree, or a hash? No!
- It must be that
operator++
is overloaded.
- Can’t do that for a plain pointer—C++ is extensible, not mutable.
- Therefore, many iterators cannot be plain pointers.
iterator vs. pointer
- All pointers can be considered iterators.
- Not all iterators are pointers.
- Some iterators are pointers.
Iterator categories
Standard iterator categories, least to most powerful:
- ForwardIterator (forward_list, unordered_set)
- can go forward by single steps
*
, ->
, =
, ==
, !=
, ++
(common to all iterators)
- BidirectionalIterator (list, set, map)
- goes forward & backward by single steps
*
, ->
, =
, ==
, !=
, ++
(common to all iterators)
--
- RandomAccessIterator (std::array, vector, string, deque)
- goes forward & backward by big steps
*
, ->
, =
, ==
, !=
, ++
(common to all iterators)
--
,
+=
int, -=
int,
+
int, -
int,
iter-
iter,
<
, <=
,
>
, >=
,
Not C++ types: descriptions, used to categorize iterators.
Iterator categories
++
works on all iterators, but +
and +=
only work on
RandomAccessIterators:
list<int> li{11,22,33,44,55,66,77,88,99};
auto b = li.begin();
b += 5; // 🦡
cout << *b;
c.cc:3: error: no match for ‘operator+=’ in ‘b += 5’ (operand types are
‘std::_List_iterator<int>’ and ‘int’)
But, sometimes, you want to do that, even if it’s inefficient:
list<int> li{00,11,22,33,44,55,66};
auto b = li.begin();
advance(b, 5);
cout << *b;
55
advance() just works, no matter what the type of the
iterator. It does simple addition for a RandomAccessIterator, and loops
otherwise.
next() is like advance(), but it returns the modified iterator:
list<int> li{00,11,22,33,44,55,66};
auto b = li.begin();
cout << *(next(b, 5));
55
next() does not modify its iterator argument. Think of
advance() as +=
, whereas next() is more like +
.
begin()
& end()
.begin()
& .end()
return iterators.
Not necessarily pointers.
.begin()
is, conceptually, a pointer to the
first element of the container.
.end()
is, conceptually, a pointer one past
the end of the container.
- It’s a half-open interval!
string s = "bonehead";
cout << "First char: " << *s.begin() << '\n';
cout << "Badness: " << *s.end() << '\n';
First char: b
Badness: ␀
string s = "genius";
cout << "First char: " << *s.begin() << '\n';
cout << "Last char: " << *(s.end()-1) << '\n';
First char: g
Last char: s
Intervals
Let’s get the mathematical important concept of a half-open interval clear:
- [1.0, 3.0] is a closed interval :
- starts with/including 1.0
- ends with/including 3.0
- (4.0, 4.5) is an open interval :
- starts after/excluding 4.0
- ends before/excluding 4.5
- [5.2, 7.0) is a half-open interval :
- starts with/including 5.2
- ends before/excluding 7.0
- 6.99999999999999 is included; 7.0 is not.
- C++ loves the last one, but for discrete
locations, not real numbers.
Intervals
It’s all about [square brackets]
vs. (round parens).
- [a,b] includes a and b, and all the numbers in between.
- An interval always includes all the numbers in between,
so we’ll stop saying that.
- (a,b) doesn’t include a, and doesn’t include b.
- [a,b) includes a, but doesn’t include b.
- This is a half-open interval—very popular in C++.
- None of these are C++ syntax—just mathematical notation.
Half-open intervals
Most C++ containers have ctors that take half-open intervals.
string alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string::iterator c = alpha.begin()+2, f = c+3;
string foo(c, f);
cout << *c << ' ' << *f << ' ' << foo << '\n';
C F CDE
-or-
string alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
auto c = &alpha[2], f = c+3;
cout << *c << ' ' << *f << ' ' << string(c,f) << '\n';
C F CDE
foo
is not CDEF
, because it’s a half-open interval.
Are both f
variables the same type?
The first is string::iterator
, the second is char *.
Are those the same?
Not necessarily. Maybe so, maybe no.
Half-open Intervals
int a[] = {00, 11, 22, 33, 44, 55, 66, 77};
vector<int> v(a+2, a+5);
for (auto n : v)
cout << n << ' ';
22 33 44
00 | 11 | 22 | 33 | 44 | 55 | 66 | 77 |
| | ↑ | | | ↑ | | |
| | start | | | end | | |
The end location is not included.
.front()
& .back()
Some containers have .front()
and .back()
, which return
references to the first and last elements.
list<double> c = {2.3, 5.7, 11.13, 17.19, 23.29};
cout << "First: " << c.front() << '\n'
<< "Last: " << c.back() << '\n';
First: 2.3
Last: 23.29
- These are not iterators; they are references to the actual data.
- The return type of
list<double>::front()
is double&
,
not list<double>::iterator
.
- Note the difference between the types double and
double&
.
string action = "Gripping";
action.front() = 'T';
cout << action;
Tripping
Comparisons, part one
This won’t work:
list<string> l = {"kappa", "alpha", "gamma"};
for (auto it = l.begin(); it < l.end(); ++it) // 🦡
cout << *it << ' ';
c.cc:2: error: no match for ‘operator<’ in ‘it <
l.std::__cxx11::list<std::__cxx11::basic_string<char> >::end()’ (operand
types are ‘std::_List_iterator<std::__cxx11::basic_string<char> >’ and
‘std::__cxx11::list<std::__cxx11::basic_string<char> >::iterator’)
Read the message. It says that <
isn’t defined for those iterators.
Comparisons, part two
This will work:
list<string> l = {"kappa", "alpha", "gamma"};
for (auto it = l.begin(); it != l.end(); ++it)
cout << *it << ' ';
kappa alpha gamma
list<>::iterator
is only a BidirectionalIterator, not a
RandomAccessIterator, and so <
isn’t defined. What would it
compare? The addresses of the linked list nodes? That’s not useful.
Constructors
Nearly all containers accept a pair of iterators as ctor arguments.
These do not have to be iterators for the same type of container.
char date[] = __DATE__; // E.g., Aug 30 2024
string now(date, date+6);
cout << "month & day: " << now << '\n';
string day_of_month(now.begin()+4, now.begin()+6);
cout << "day: " << day_of_month << '\n';
multiset<int> ms(now.begin(), now.end());
for (auto n : ms)
cout << n << ' ';
cout << '\n';
month & day: Aug 30
day: 30
32 48 51 65 103 117
begin()
and end()
functions
Let’s copy from a C array to a C++ string:
int fido[] = {'d','o','g'};
string current(fido, fido+2);
cout << current << '\n';
do
Oh dear, I counted wrong. Why am I counting!? Am I a computer?
int fido[] = {'d','o','g'};
string current(fido.begin(), fido.end()); // 🦡
cout << current << '\n';
c.cc:2: error: request for member ‘begin’ in ‘fido’, which is of
non-class type ‘int [3]’
Alas, fido
has no methods.
There are also free functions begin() and end(),
which work on arrays (not pointers) and all standard containers:
int fido[] = {'d','o','g'};
string current(begin(fido), end(fido));
cout << current << '\n';
dog
Try that again
It was crazy to separate char values. Let’s just
use a C string:
char fido[] = "DOG";
string current(begin(fido), end(fido));
cout << current << '\n';
DOG␀
␀? What fresh hell is this?
␀ is how these web pages display '\0'
, the null character.
What is sizeof(fido)
?
Did you count the null character ('\0'
) at the end? We’re not
asking for the length of the C-string, which is clearly 3. Instead,
we’re asking how many bytes the variable fido
occupies: 4.
Pointer invalidation
Consider this poor code:
int *p = new int(42);
cout << "Before: " << *p << '\n';
delete p;
cout << "After: " << *p << '\n';
Before: 42
After: 5525
- Nothing mysterious here.
p
was a valid pointer, then it became
invalid. It’s a “dangling pointer”.
- Just because a pointer exists, doesn’t mean that it points
to something that you may access.
- I know the address of the White House, but they won’t let me move in.
Iterator invalidation
vector<long> v = {253};
vector<long>::iterator it = v.begin();
cout << "Before: " << *it << '\n';
for (long i=1; i<1000; i++)
v.push_back(i);
cout << "After: " << *it << '\n';
Before: 253
After: 9468
- Sure,
it
“pointed” to v[0]
just fine, at first.
- As we added all those elements to
v
,
unavoidably, v
’s data got reallocated to make more room.
- The poor iterator referring to
v[0]
continued to point to
the old, obsolete, location.
Reservation
Using .reserve()
pre-allocates memory:
vector<long> v = {253};
v.reserve(1005);
auto it = v.begin();
cout << "Before: " << *it << '\n';
for (long i=1; i<1000; i++)
v.push_back(i);
cout << "After: " << *it << '\n';
Before: 253
After: 253
How often?
How often does re-allocation happen? We can find out,
for any particular implemention :
vector<int> v;
for (int i=1; i<=1000; i++) {
auto before = v.capacity();
v.push_back(i);
auto after = v.capacity();
if (before != after)
cout << i << ' ' << after << '\n';
}
1 1
2 2
3 4
5 8
9 16
17 32
33 64
65 128
129 256
257 512
513 1024
.capacity()
: how much memory is allocated
.size()
: how much memory is used
How often?
Similarly, because a std::string is not much more
than a vector<char>
:
string s;
cout << s.size() << ' ' << s.capacity() << '\n';
for (int i=1; i<10'000; i++) {
auto before = s.capacity();
s += 'x';
auto after = s.capacity();
if (before != after)
cout << s.size() << ' ' << after << '\n';
}
0 15
16 30
31 60
61 120
121 240
241 480
481 960
961 1920
1921 3840
3841 7680
7681 15360
What happened to our nice powers of two?
Curious String Behavior
for (string s; s.size()<60; s+="abcde")
cout << s.size() << ' ' << (void *) s.data() << '\n';
0 0x7ffda7587680
5 0x7ffda7587680
10 0x7ffda7587680
15 0x7ffda7587680
20 0x9fb2c0
25 0x9fb2c0
30 0x9fb2c0
35 0x9fb2f0
40 0x9fb2f0
45 0x9fb2f0
50 0x9fb2f0
55 0x9fb2f0
Casting? Why can’t we use << s.data()
or << &s[0]
to get the address of the string data?
Small String Optimization
- This is the popular small string optimization.
- Big strings (>15 bytes, for g++ 8) are stored in the heap,
as usual.
- Small strings are kept right in the string object,
in the same space as the pointer and lengths.
- Small strings are much faster:
everything’s on the stack, in cache, no pointers!
Small String Optimization
Possible small string implementation:
class string {
private:
size_t size; // actual string length
union {
struct {
size_t alloc_size; // amount of heap data
char *heap; // ptr to heap data
} s;
char local[16]; // or, put it here
};
public:
char& operator[](size_t i) {
return (i < sizeof(local)) ? local[i] : s.heap[i];
}
// and many more methods
};
Order Calculation
- Still, the small string optimization only applies to,
well, small strings.
- Bigger data still undergoes reallocation and copying.
- All that reallocation looks expensive.
- However, science is better than first impressions.
- What is the big-O time cost for adding n items to a vector?
Big-O
- Let’s push 0…77 onto a vector. Reallocation will occur
at power of two sizes:
- Push 0: grow to size=1, copy nothing
- Push 1: grow to size=2, copy 1 int
- Push 2: grow to size=4, copy 2 ints
- Push 4: grow to size=8, copy 4 ints
- Push 8: grow to size=16, copy 8 ints
- Push 16: grow to size=32, copy 16 ints
- Push 32: grow to size=64, copy 32 ints
- Push 64: grow to size=128, copy 64 ints
- That’s 1+2+4+8+16+32+64 copies for 78 ints, 127÷78≈1.6 copies/int. O(1)!
Does it scale?
vector<char> v;
int copies = 0, iterations = 1'000'000;
for (int i=0; i<iterations; i++) {
auto before = v.capacity();
v.push_back(i);
if (before != v.capacity())
copies += before;
}
cout << double(copies)/iterations;
1.04858
Pre-allocation helps
Again, if we know how many items we’re going to add, we can
.reserve()
the space:
vector<int> v;
v.reserve(900);
cout << v.size() << ' ' << v.capacity() << '\n';
for (int i=1; i<1000; i++) {
auto before = v.capacity();
v.push_back(i);
auto after = v.capacity();
if (before != after)
cout << v.size() << ' ' << after << '\n';
}
0 900
901 1800
Why does this fail?
class Foo {
int sum() const {
int total = 0;
for (vector<int>::iterator it = data.begin(); it != data.end(); ++it) // 🦡
total += *it;
return total;
}
vector<int> data;
};
c.cc:4: error: conversion from ‘__normal_iterator<const int*,[...]>’ to
non-scalar type ‘__normal_iterator<int*,[...]>’ requested
.sum()
is a const method.
- Therefore, inside
.sum()
, data
is treated as
a const vector<int>
.
- How is a const method implemented, anyway?
- It’s not that the compiler checks for changes to member data
inside a const method.
- Instead, the compiler simply regards all data members as const
when compiling a const method.
- Therefore,
data.begin()
and data.end()
return iterators
of type const_iterator
, not type iterator
.
- They’re overloaded methods!
- You can’t assign
const_iterator
to iterator
.
Restate the problem
This is the same problem:
const vector<int> v = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) // 🦡
cout << *it << ' ';
c.cc:2: error: conversion from ‘__normal_iterator<const int*,[...]>’ to
non-scalar type ‘__normal_iterator<int*,[...]>’ requested
Solution #1
One solution—use the correct type:
const vector<int> v = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (vector<int>::const_iterator it = v.begin(); it != v.end(); ++it)
cout << *it << ' ';
1 1 2 3 5 8 13 21 34
Solution #2
A better solution—let auto figure it out:
const vector<int> v = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto it = v.begin(); it != v.end(); ++it)
cout << *it << ' ';
1 1 2 3 5 8 13 21 34
Solution #3
The best solution—avoid all of this:
const vector<int> v = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto val : v)
cout << val << ' ';
1 1 2 3 5 8 13 21 34
.cbegin()
and .cend()
.begin()
is an overloaded method. It returns a const_iterator
if the object is const, iterator
otherwise.
.end()
is an overloaded method. It returns a const_iterator
if the object is const, iterator
otherwise.
.cbegin()
: like .begin()
, but always returns const_iterator
.cend()
: like .end()
, but always returns const_iterator
vector<int> v = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto it = v.cbegin(); it != v.cend(); ++it)
cout << *it << ' ';
1 1 2 3 5 8 13 21 34
.rbegin()
and .rend()
.rbegin()
: like .begin()
, but goes the other way
.rend()
: like .end()
, but goes the other way
.crbegin()
: like .cbegin()
, but goes the other way
.crend()
: like .cend()
, but goes the other way
array<int, 9> v = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto it = v.rbegin(); it != v.rend(); ++it)
cout << *it << ' ';
34 21 13 8 5 3 2 1 1
list<int> v = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto it = v.crbegin(); it != v.crend(); ++it)
cout << *it << ' ';
34 21 13 8 5 3 2 1 1
Don’t get too excited
- Not all containers have the reversed versions.
- For example, forward_list, as a singly-linked list,
doesn’t have reverse iterators.
- For some containers,
iterator
and const_iterator
are the same.
- A set has inherently-constant iterators, because it’s in order.
If you could change the values via iterators, then it wouldn’t be in
order any more.
- An unordered_set, a hash, has its own special order, and
changing the values in place would be bad.
Plain old data types:
How about old C-style data?
int a[] = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto it = a.begin(); it != a.end(); ++it) // 🦡
cout << *it << ' ';
c.cc:2: error: request for member ‘begin’ in ‘a’, which is of non-class
type ‘int [9]’
That failed miserably. Perhaps it’s because arrays are not objects.
Free functions
Fortunately, the begin(), end(), and size() free functions
work for containers and C arrays.
begin(thing)
is defined as, conceptually
(through the magic of templates):
if thing is a C-style array
then
address of the start of the array
else
thing.begin()
Similarly, end(thing)
is:
if thing is a C-style array
then
address just after the end of the array
else
thing.end()
Free functions
int a[] = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto it = begin(a); it != end(a); ++it)
cout << *it << ' ';
1 1 2 3 5 8 13 21 34
These work for objects, as well:
deque<int> s = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto it = begin(s); it != end(s); ++it)
cout << *it << ' ';
1 1 2 3 5 8 13 21 34
What use is that? Generality for the sake of generality? No, it’s so
that for loops will work for arrays as well as objects (even
user-defined classes) with .begin()
and .end()
methods.
for loop
Consider any for loop:
forward_list<int> fl = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (double v : fl)
cout << v << ' ';
1 1 2 3 5 8 13 21 34
The compiler turns that into something like this, with free functions, not methods:
forward_list<int> fl = {1, 1, 2, 3, 5, 8, 13, 21, 34};
for (auto iter = begin(fl), e = end(fl); iter != e; ++iter) {
double v = *iter;
cout << v << ' ';
}
1 1 2 3 5 8 13 21 34
which works for any container type, even C-style arrays.
end() is efficiently called only once.
Efficiency
class Foo {
public:
Foo& operator++() {
// whatever needs to be done
return *this;
}
Foo operator++(int) {
const auto save = *this;
++*this;
return save;
}
};
- Why do our examples use preincrement (
++it
) instead of postincrement
(it++
)?
- Postincrement is generally slower—it copies and preincrements.
- Even if efficiency isn’t a consideration, there’s no clear readability
difference, so we may as well be fast.
- A smart compiler might make them equivalent, if it notices that
the object that postdecrement returns is not used.
- For actual pointers (int *) or integers, do what you like.
Some programmers always use preincrement, as a good habit.
Google has this as a rule.