C++ Pre-Increment VS Post-Increment

Introduction

When I was programming C in college, I wrote i++ or ++i interchangeably, especially in a for loop, because I know their performance and consequence are exactly the same. However, “good” C++ programmers almost never write i++.

In this blog post, I would like to discuss the difference between i++ or ++i and the best practice for performance in C++.

C++ VS ++C

There is a difference between the consequence of i++ or ++i if their return values are being assigned or copied to variables. In this article, we are only interested in talking about i++ or ++i when their return values are not being assigned or copied to variables.

C

In C, the pre-increment and post-increment ++ are defined for built-in primitive types. When a compiler sees the code containing i++, such as

1
2
3
4
for (int i; i < 10; i++)
{
...
}

Because the return value of i++, the value of i before increment, was never assigned, the copy and return operation might just be casted away by the compiler, and i++ becomes exactly the same as ++i.

Notice that there is no operator overloading in C, and the pre-increment and post-increment ++ are only defined for built-in primitive types. Therefore, there is no story about the performance of the pre-increment and post-increment of user defined types in C.

C++

In C++, if the variable is of built-in primitive types, the pre-increment and post-increment ++ can also be optimized by the compiler similarly as in C. However, because C++ supports operator overloading, therefore the pre-increment and post-increment ++ can also be defined for user defined types.

For example, we have the following Int class and we have defined the pre-increment and post-increment ++ operators for it. A more complicated non-primitive type which supports ++ in C++ STL is iterator.

int.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

class Int
{
public:
Int(int x) : m_x{x}
{
}
Int(const Int& other) : m_x{other.m_x}
{
std::cout << "Copy constructor being called." << std::endl;
}
// Pre-increment
Int& operator++()
{
++m_x;
return *this;
}
// Post-increment
Int operator++(int /* not used */)
{
Int tmp{*this};
++(*this);
return tmp;
}

private:
int m_x;
};

int main()
{
Int i{0};
i++;
++i;
}

Because the pre-increment and post-increment ++ are inlined, i.e., the definition and the declaration are together, the compiler would see the definition of the post-increment ++ when generating the code for i++, therefore, it is possible that the compiler could optimize away the copy of the object in the post-increment member function for the i++ code.

However, in this case, GCC did not optimize the copy of the object away in the post-increment member function for the i++ code, even if GCC could.

1
2
3
$ g++ int.cpp -o int -std=c++14
$ ./int
Copy constructor being called.

If the pre-increment and post-increment ++ were are not inlined, it is impossible for the compiler to optimize away the copy of the object in the post increment function.

For example, if the Int class was declared in a header int.h file and defined in another file int.cpp, the compiler cannot optimize the post-increment ++ by just looking at the header declaration.

int.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

class Int
{
public:
Int(int x);
Int(const Int& other);
// Pre-increment
Int& operator++();
// Post-increment
Int operator++(int /* not used */);

private:
int m_x;
};
int.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "int.h"

Int::Int(int x) : m_x{x}
{
}

Int::Int(const Int& other) : m_x{other.m_x}
{
std::cout << "Copy constructor being called." << std::endl;
}

// Pre-increment
Int& Int::operator++()
{
++m_x;
return *this;
}

// Post-increment
Int Int::operator++(int /* not used */)
{
Int tmp{*this};
++(*this);
return tmp;
}

The member functions are not inlined in this case either, even if the declaration and the definition are in the same header file int.h.

int.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

class Int
{
public:
Int(int x);
Int(const Int& other);
// Pre-increment
Int& operator++();
// Post-increment
Int operator++(int /* not used */);

private:
int m_x;
};

Int::Int(int x) : m_x{x}
{
}

Int::Int(const Int& other) : m_x{other.m_x}
{
std::cout << "Copy constructor being called." << std::endl;
}

// Pre-increment
Int& Int::operator++()
{
++m_x;
return *this;
}

// Post-increment
Int Int::operator++(int /* not used */)
{
Int tmp{*this};
++(*this);
return tmp;
}

Only by adding the inline keyword would the compiler inline the member functions. In this case, the compiler has a chance to optimize away the copy of the object in the post increment function.

int.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>

class Int
{
public:
Int(int x);
Int(const Int& other);
// Pre-increment
Int& operator++();
// Post-increment
Int operator++(int /* not used */);

private:
int m_x;
};

Int::Int(int x) : m_x{x}
{
}

Int::Int(const Int& other) : m_x{other.m_x}
{
std::cout << "Copy constructor being called." << std::endl;
}

// Pre-increment
inline
Int& Int::operator++()
{
++m_x;
return *this;
}

// Post-increment
inline
Int Int::operator++(int /* not used */)
{
Int tmp{*this};
++(*this);
return tmp;
}

Conclusions

The performance of i++ would never be better than the performance of ++i in C++ if their return values are never being assigned or copied to variables. In this scenario, we should only write ++i in C++. Only write i++ when we want to assign or copy the value of i before increment in C++.

References

Author

Lei Mao

Posted on

02-13-2022

Updated on

02-13-2022

Licensed under


Comments