PrevNext

Debugging C++

Authors: Benjamin Qi, Aryansh Shrivastava

Debugging Tips Specific to C++

Edit This Page

Checking for Out of Bounds

Writing to an out of bounds array index is known as buffer overflow. C++ may or may not produce a runtime error upon buffer overflow. For example, the following code results in a runtime error on ide.usaco.guide, but outputs 4 on my computer.

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> invalid_vec{1};
vector<int> valid_vec{1234};
cout << valid_vec[0] << "\n"; // outputs 1234
for (int i = 0; i < 10; i++) {
invalid_vec[i] = i; // may or may not error
}
cout << valid_vec[0] << "\n"; // may output 4
}

To ensure that an error is raised when accessing an out of bounds index, you can use vector::at instead of vector::operator[] like so:

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> invalid_vec{1};
vector<int> valid_vec{1234};
cout << valid_vec.at(0) << "\n"; // outputs 1234
for (int i = 0; i < 10; i++) {
invalid_vec.at(i) = i; // throws std::out_of_range
}
cout << valid_vec.at(0) << "\n";
}

C++ will now check the bounds when we access the vectors and will produce the following output:

1234
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 1) >= this->size() (which is 1)
1 zsh: abort      ./$1 $@[2,-1]

Line Numbers

Note that the output above does not contain the line number where the runtime error occurred. To output the line number, you can use a debugger such as gdb or lldb. See the section on debuggers for more information.

Unspecified Evaluation Order

Here is some unexpected behavior you might come across when trying to create a trie or a persistent segment tree.

#include <bits/stdc++.h>
using namespace std;
vector<int> res{-1};
int add_element() {
res.push_back(-1);
return res.size() - 1; // index of added element
}

Compiling and running the above code with -std=c++17 gives the intended output:

0 1
1 2
2 3
3 4
4 5

But compiling and running with -std=c++14 gives something unexpected:

0 -1
1 -1
2 3
3 -1
4 5

For both -std=c++17 and -std=c++14, the intended output is produced if the result of add_element() is saved to a temporary variable.

#include <bits/stdc++.h>
using namespace std;
vector<int> res{-1};
int add_element() {
res.push_back(-1);
return res.size() - 1; // index of added element
}

The problem is that res[i] = add_element(); only works if add_element() is evaluated before res[i] is. If res[i] is evaluated first, and then add_element() results in the memory for res being reallocated, then res[i] is invalidated. The order in which res[i] and add_element() are evaluated is unspecified (at least before C++17).

See this StackOverflow post for some discussion about why this is the case (here's a similar issue).

GCC Warning Options

In this section and the following one we'll go over options you can add to your g++ compile command to aid in debugging.

Resources
CF

Includes all the options below.

GCC

Official documentation for the options below.

Here are the warning options that Ben uses:

-Wall -Wextra -Wshadow -Wconversion -Wfloat-equal -Wduplicated-cond -Wlogical-op

We give examples for some of these below.

For Users of USACO Guide IDE

You can customize your compilation options in ide.usaco.guide.

Compilation Options Used by Top Competitive Programmers

You can check which options Errichto and ecnerwala use.

-Wall

Enables many (but not all) warning options, including -Wuninitialized and -Wunused-variable.

#include <bits/stdc++.h>
using namespace std;
int main() {
int x;
cout << x;
}

Compile output:

main.cpp: In function ‘int main()’:
main.cpp:6:10: warning: ‘x’ is used uninitialized in this function [-Wuninitialized]
    6 |  cout << x;
      |          ^

-Wextra

Enables some warning options not enabled by -Wall, such as -Wmissing-field-initializers.

#include <bits/stdc++.h>
using namespace std;
struct s { int f, g, h; };
int main() {
s x = { 3, 4 };
}

Compile output:

main.cpp: In function ‘int main()’:
main.cpp:7:18: warning: missing initializer for member ‘s::h’ [-Wmissing-field-initializers]
    7 |     s x = { 3, 4 };
      |                  ^

-Wconversion

Warns for implicit conversions that may alter a value.

#include <bits/stdc++.h>
using namespace std;
int main() {
double x = 5.5;
int y = x;
cout << y;
}

Compile output:

main.cpp: In function ‘int main():
main.cpp:6:13: warning: conversion from ‘double’ to ‘int’ may change value [-Wfloat-conversion]
6 | int y = x;
| ^

-Wshadow

#include <bits/stdc++.h>
using namespace std;
int x;
int main() {
int x = 5;
cout << x;
}

Compile Output:

main.cpp: In function ‘int main():
main.cpp:7:6: warning: declaration of ‘x’ shadows a global declaration [-Wshadow]
7 | int x = 5;
| ^
main.cpp:4:5: note: shadowed declaration is here
4 | int x;
| ^

-Wfloat-equal

Warns if floating-point values are used in equality comparisons.

#include <bits/stdc++.h>
using namespace std;
int main() {
double x = 1.0 / 49 * 49;
cout << (x == 1.0); // 0
}

Compile Output:

main.cpp: In function ‘int main():
main.cpp:6:16: warning: comparing floating-point with ‘==or!=’ is unsafe [-Wfloat-equal]
6 | cout << (x == 1.0);
| ~~^~~~~~

GCC Debug Options

Warning!

These can slow down compilation time even runtime, so don't enable these when speed is of the essence (e.g., for Facebook Hacker Cup).

Warning!

-fsanitize flags don't work with MinGW. If you're using Windows but still want to use these flags, consider using an online compiler (or installing Linux) instead.

-fsanitize=undefined

Example: Array Out of Bounds

Without -fsanitize=undefined, this program executes successfully and outputs garbage:

#include <bits/stdc++.h>
using namespace std;
int main() {
int v[5];
cout << v[5] << endl; // may output an arbitrary integer
}

With -fsanitize=undefined, this program still executes successfully, but the following runtime error is printed to standard error:

main.cpp:6:13: runtime error: index 5 out of bounds for type 'int [5]'
main.cpp:6:13: runtime error: load of address 0x7ffc4efaf2d4 with insufficient space for an object of type 'int'
0x7ffc4efaf2d4: note: pointer points here
  11 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  f0 15 40 00 00 00 00 00  00 00 00 00 00 00 00 00
              ^ 

Example: Vector Out of Bounds

The code below produces a segmentation fault:

#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v;
cout << v[-1] << endl;
}

Output:

/tmp/program/run.sh: line 1:    71 Segmentation fault      ./prog
Command exited with non-zero status 139

With -fsanitize=undefined, a slightly more informative error message is produced:

/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/stl_vector.h:1046:34: runtime error: applying non-zero offset 18446744073709551612 to null pointer
/tmp/program/run.sh: line 1:  1845 Segmentation fault      ./prog
Command exited with non-zero status 139

Example: Integer Overflow

#include <bits/stdc++.h>
using namespace std;
int main() {
int x = 1 << 30;
cout << x + x << endl;
}

With -fsanitize=undefined, this program still executes successfully, but the following runtime error is printed to standard error:

main.cpp:6:14: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'

Example: Detecting Multiple Errors

By default, the undefined behavior sanitizer will attempt to continue after detecting an error. For example, the following program with -fsanitize=undefined produces multiple runtime errors:

#include <bits/stdc++.h>
using namespace std;
int main() {
cout << (1 << 32) << endl;
cout << (1 << 32) << endl;
cout << (1 << 32) << endl;
}

Standard Error:

main.cpp:5:13: runtime error: shift exponent 32 is too large for 32-bit type 'int'
main.cpp:6:13: runtime error: shift exponent 32 is too large for 32-bit type 'int'
main.cpp:7:13: runtime error: shift exponent 32 is too large for 32-bit type 'int'

To disable this behavior and exit after the first detected error, we can use -fsanitize=undefined with -fno-sanitize-recover.

Standard Error:

main.cpp:5:13: runtime error: shift exponent 32 is too large for 32-bit type 'int'
Command exited with non-zero status 1

-fsanitize=address

Resources
GCC

documentation for -g

Example: Vector Out of Bounds

Recall that this example from the previous subsection gives a segmentation fault:

#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v;
cout << v[-1] << endl;
}

Compiling with -fsanitize=address gives:

Standard Error

For more helpful information we should additionally compile with the -g flag, which generates a file containing debugging information based on the line numbering of the program.

Standard Error

Example: Array Out of Bounds

#include <bits/stdc++.h>
using namespace std;
int main() {
int v[5];
cout << v[5] << endl;
}

With -fsanitize=address -g:

Standard Error

-D_GLIBCXX_DEBUG

This enables debug mode, which replaces each STL container with its corresponding debug container.

Resources
GCC

documentation for -D_GLIBCXX_DEBUG

Recall that the following program gives a segmentation fault.

#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v;
cout << v[-1] << endl;
}

With -D_GLIBCXX_DEBUG the following output is produced:

Debug

Using the LLDB Debugger

Recall the example from Checking Out of Bounds section where the output didn't contain the number of the line where the runtime error occurred. Below, we show how to use lldb to output the line number. Assume the C++ source code is named prog.cpp and the executable is named prog.

  1. Add -g to the compile command and compile.
  2. Start debug mode on prog using lldb prog.
  3. Start running the program using r.
  4. Show the stack backtrace using bt.

Output

As mentioned in the previous module, there are many more things you can do with debuggers, but they aren't particularly useful for competitive programming.

Module Progress:

Join the USACO Forum!

Stuck on a problem, or don't understand a module? Join the USACO Forum and get help from other competitive programmers!

PrevNext