Coming Soon
This lesson is currently being developed
More debugging tactics
Learn practical techniques for debugging C++ programs.
What to Expect
Comprehensive explanations with practical examples
Interactive coding exercises to practice concepts
Knowledge quiz to test your understanding
Step-by-step guidance for beginners
Development Status
Content is being carefully crafted to provide the best learning experience
Preview
Early Preview Content
This content is still being developed and may change before publication.
3.5 — More debugging tactics
In this lesson, you'll learn advanced debugging tactics that build upon the basics. These techniques are particularly useful for complex bugs that aren't easily found with simple print statements.
Using test-driven debugging
Test-driven debugging involves creating specific tests that reproduce the bug, then using those tests to verify your fix.
Creating targeted test cases
#include <iostream>
#include <vector>
#include <cassert>
// Bug: This function should remove all occurrences of a value
void removeValue(std::vector<int>& vec, int valueToRemove)
{
// Original buggy implementation - only removes first occurrence
/*
for (auto it = vec.begin(); it != vec.end(); ++it)
{
if (*it == valueToRemove)
{
vec.erase(it);
break; // BUG: Only removes first occurrence
}
}
*/
// Another buggy attempt - iterator invalidation
/*
for (auto it = vec.begin(); it != vec.end(); ++it)
{
if (*it == valueToRemove)
{
vec.erase(it); // BUG: Invalidates iterator, causes undefined behavior
}
}
*/
// Correct implementation
auto it = vec.begin();
while (it != vec.end())
{
if (*it == valueToRemove)
{
it = vec.erase(it); // erase returns iterator to next element
}
else
{
++it;
}
}
}
void printVector(const std::vector<int>& vec, const std::string& label)
{
std::cout << label << ": ";
for (int value : vec)
{
std::cout << value << " ";
}
std::cout << std::endl;
}
void testRemoveValue()
{
std::cout << "=== Test-Driven Debugging for removeValue ===" << std::endl;
// Test case 1: Remove single occurrence
{
std::vector<int> vec = {1, 2, 3, 4, 5};
printVector(vec, "Before removing 3");
removeValue(vec, 3);
printVector(vec, "After removing 3");
// Verify result
std::vector<int> expected = {1, 2, 4, 5};
assert(vec == expected && "Single occurrence removal failed");
std::cout << "✓ Test 1 passed\n" << std::endl;
}
// Test case 2: Remove multiple occurrences (this would fail with buggy version)
{
std::vector<int> vec = {1, 2, 2, 3, 2, 4};
printVector(vec, "Before removing all 2s");
removeValue(vec, 2);
printVector(vec, "After removing all 2s");
// Verify result
std::vector<int> expected = {1, 3, 4};
assert(vec == expected && "Multiple occurrence removal failed");
std::cout << "✓ Test 2 passed\n" << std::endl;
}
// Test case 3: Remove from empty vector
{
std::vector<int> vec = {};
removeValue(vec, 5);
assert(vec.empty() && "Empty vector test failed");
std::cout << "✓ Test 3 passed\n" << std::endl;
}
// Test case 4: Remove value not in vector
{
std::vector<int> vec = {1, 3, 5, 7};
std::vector<int> original = vec; // Keep copy
removeValue(vec, 2);
assert(vec == original && "Remove non-existent value failed");
std::cout << "✓ Test 4 passed\n" << std::endl;
}
// Test case 5: Remove all elements
{
std::vector<int> vec = {7, 7, 7};
removeValue(vec, 7);
assert(vec.empty() && "Remove all elements failed");
std::cout << "✓ Test 5 passed\n" << std::endl;
}
}
int main()
{
testRemoveValue();
std::cout << "All tests passed!" << std::endl;
return 0;
}
Output:
=== Test-Driven Debugging for removeValue ===
Before removing 3: 1 2 3 4 5
After removing 3: 1 2 4 5
✓ Test 1 passed
Before removing all 2s: 1 2 2 3 2 4
After removing all 2s: 1 3 4
✓ Test 2 passed
✓ Test 3 passed
✓ Test 4 passed
✓ Test 5 passed
All tests passed!
Regression testing
Create tests that ensure fixes don't break existing functionality.
Building a regression test suite
#include <iostream>
#include <cmath>
#include <iomanip>
// Mathematical functions that need careful testing
class MathUtils
{
public:
// Bug history: This had an off-by-one error in factorial calculation
static long long factorial(int n)
{
if (n < 0) return -1; // Error indicator
if (n <= 1) return 1;
long long result = 1;
for (int i = 2; i <= n; ++i) // Fixed: was i < n (off-by-one)
{
result *= i;
}
return result;
}
// Bug history: This didn't handle negative bases correctly
static double power(double base, int exponent)
{
if (exponent == 0) return 1.0;
if (exponent == 1) return base;
bool negativeExponent = (exponent < 0);
int absExponent = std::abs(exponent);
double result = 1.0;
for (int i = 0; i < absExponent; ++i)
{
result *= base;
}
return negativeExponent ? (1.0 / result) : result;
}
// Bug history: Had precision issues with floating-point comparison
static bool isPerfectSquare(double number)
{
if (number < 0) return false;
double root = std::sqrt(number);
double rounded = std::round(root);
// Fixed: Use epsilon for floating-point comparison
const double epsilon = 1e-10;
return std::abs(root - rounded) < epsilon;
}
};
class RegressionTests
{
private:
static int testsPassed;
static int testsTotal;
public:
static void assertEqual(long long actual, long long expected, const std::string& testName)
{
testsTotal++;
if (actual == expected)
{
std::cout << "✓ " << testName << " passed" << std::endl;
testsPassed++;
}
else
{
std::cout << "✗ " << testName << " FAILED: expected " << expected
<< ", got " << actual << std::endl;
}
}
static void assertEqual(double actual, double expected, const std::string& testName, double tolerance = 1e-10)
{
testsTotal++;
if (std::abs(actual - expected) < tolerance)
{
std::cout << "✓ " << testName << " passed" << std::endl;
testsPassed++;
}
else
{
std::cout << "✗ " << testName << " FAILED: expected " << std::fixed << std::setprecision(10)
<< expected << ", got " << actual << std::endl;
}
}
static void assertEqual(bool actual, bool expected, const std::string& testName)
{
testsTotal++;
if (actual == expected)
{
std::cout << "✓ " << testName << " passed" << std::endl;
testsPassed++;
}
else
{
std::cout << "✗ " << testName << " FAILED: expected " << std::boolalpha
<< expected << ", got " << actual << std::endl;
}
}
static void runAllTests()
{
testsPassed = 0;
testsTotal = 0;
std::cout << "=== Regression Test Suite ===" << std::endl;
testFactorial();
testPower();
testIsPerfectSquare();
std::cout << "\n=== Test Results ===" << std::endl;
std::cout << "Tests passed: " << testsPassed << "/" << testsTotal << std::endl;
if (testsPassed == testsTotal)
{
std::cout << "🎉 All regression tests passed!" << std::endl;
}
else
{
std::cout << "❌ Some tests failed - regression detected!" << std::endl;
}
}
private:
static void testFactorial()
{
std::cout << "\nTesting factorial function:" << std::endl;
// Basic cases
assertEqual(MathUtils::factorial(0), 1LL, "factorial(0)");
assertEqual(MathUtils::factorial(1), 1LL, "factorial(1)");
assertEqual(MathUtils::factorial(5), 120LL, "factorial(5)");
// Edge cases
assertEqual(MathUtils::factorial(-1), -1LL, "factorial(-1) error handling");
// Regression test: This would fail if off-by-one bug returned
assertEqual(MathUtils::factorial(4), 24LL, "factorial(4) regression test");
}
static void testPower()
{
std::cout << "\nTesting power function:" << std::endl;
// Basic cases
assertEqual(MathUtils::power(2.0, 3), 8.0, "power(2, 3)");
assertEqual(MathUtils::power(5.0, 0), 1.0, "power(5, 0)");
assertEqual(MathUtils::power(3.0, 1), 3.0, "power(3, 1)");
// Negative base
assertEqual(MathUtils::power(-2.0, 2), 4.0, "power(-2, 2)");
assertEqual(MathUtils::power(-2.0, 3), -8.0, "power(-2, 3)");
// Negative exponent
assertEqual(MathUtils::power(2.0, -2), 0.25, "power(2, -2)");
}
static void testIsPerfectSquare()
{
std::cout << "\nTesting isPerfectSquare function:" << std::endl;
// Perfect squares
assertEqual(MathUtils::isPerfectSquare(4.0), true, "isPerfectSquare(4)");
assertEqual(MathUtils::isPerfectSquare(9.0), true, "isPerfectSquare(9)");
assertEqual(MathUtils::isPerfectSquare(16.0), true, "isPerfectSquare(16)");
// Non-perfect squares
assertEqual(MathUtils::isPerfectSquare(5.0), false, "isPerfectSquare(5)");
assertEqual(MathUtils::isPerfectSquare(10.0), false, "isPerfectSquare(10)");
// Edge cases
assertEqual(MathUtils::isPerfectSquare(0.0), true, "isPerfectSquare(0)");
assertEqual(MathUtils::isPerfectSquare(-1.0), false, "isPerfectSquare(-1)");
// Regression test: Floating-point precision issues
assertEqual(MathUtils::isPerfectSquare(1.0), true, "isPerfectSquare(1.0) precision test");
}
};
int RegressionTests::testsPassed = 0;
int RegressionTests::testsTotal = 0;
int main()
{
RegressionTests::runAllTests();
return 0;
}
Code inspection techniques
Systematic code review can catch bugs that testing might miss.
Walkthrough debugging
#include <iostream>
#include <vector>
// Function with subtle bugs - let's walk through it step by step
int findKthLargest(std::vector<int>& nums, int k)
{
std::cout << "=== Code Walkthrough: findKthLargest ===" << std::endl;
std::cout << "Input: nums = [";
for (size_t i = 0; i < nums.size(); ++i)
{
std::cout << nums[i];
if (i < nums.size() - 1) std::cout << ", ";
}
std::cout << "], k = " << k << std::endl;
// Step 1: Input validation (missing in original buggy version)
std::cout << "\nStep 1: Input validation" << std::endl;
if (k <= 0 || k > static_cast<int>(nums.size()))
{
std::cout << "❌ Invalid k value: " << k << std::endl;
return -1; // Error indicator
}
std::cout << "✓ k is valid" << std::endl;
// Step 2: Sort the array (simple but inefficient approach)
std::cout << "\nStep 2: Sorting array" << std::endl;
// Simple bubble sort for educational purposes (inefficient but clear)
for (size_t i = 0; i < nums.size(); ++i)
{
for (size_t j = 0; j < nums.size() - 1 - i; ++j)
{
if (nums[j] < nums[j + 1]) // Sort in descending order
{
std::swap(nums[j], nums[j + 1]);
}
}
}
std::cout << "Sorted array (descending): [";
for (size_t i = 0; i < nums.size(); ++i)
{
std::cout << nums[i];
if (i < nums.size() - 1) std::cout << ", ";
}
std::cout << "]" << std::endl;
// Step 3: Return kth element (watch out for off-by-one!)
std::cout << "\nStep 3: Finding kth largest" << std::endl;
std::cout << "k = " << k << ", so we want index " << (k - 1) << std::endl;
int result = nums[k - 1]; // k-1 because arrays are 0-indexed
std::cout << "The " << k << "th largest element is: " << result << std::endl;
return result;
}
// Version with common bugs for comparison
int findKthLargest_buggy(std::vector<int>& nums, int k)
{
// Bug 1: No input validation
// Bug 2: Wrong index calculation
// Bug 3: Assumes sorted in ascending order
// Sort in ascending order (wrong for this purpose)
for (size_t i = 0; i < nums.size(); ++i)
{
for (size_t j = 0; j < nums.size() - 1 - i; ++j)
{
if (nums[j] > nums[j + 1])
{
std::swap(nums[j], nums[j + 1]);
}
}
}
return nums[k]; // Bug: should be k-1, and wrong end of array
}
void demonstrateWalkthrough()
{
std::cout << "=== Walkthrough Debugging Demo ===" << std::endl;
std::vector<int> test1 = {3, 2, 1, 5, 6, 4};
std::vector<int> test1_copy = test1; // Keep original for buggy version
std::cout << "\n--- Correct version walkthrough ---" << std::endl;
int result1 = findKthLargest(test1, 2);
std::cout << "\n--- Buggy version comparison ---" << std::endl;
std::cout << "Buggy version result: " << findKthLargest_buggy(test1_copy, 2) << std::endl;
std::cout << "Expected: 5 (2nd largest)" << std::endl;
}
int main()
{
demonstrateWalkthrough();
return 0;
}
Output:
=== Walkthrough Debugging Demo ===
--- Correct version walkthrough ---
=== Code Walkthrough: findKthLargest ===
Input: nums = [3, 2, 1, 5, 6, 4], k = 2
Step 1: Input validation
✓ k is valid
Step 2: Sorting array
Sorted array (descending): [6, 5, 4, 3, 2, 1]
Step 3: Finding kth largest
k = 2, so we want index 1
The 2th largest element is: 5
--- Buggy version comparison ---
Buggy version result: 4
Expected: 5 (2nd largest)
Hypothesis-driven debugging
Form and test specific hypotheses about what might be causing the bug.
Systematic hypothesis testing
#include <iostream>
#include <string>
#include <cctype>
// Bug: Password validation function sometimes accepts invalid passwords
bool isValidPassword(const std::string& password)
{
// Requirements:
// 1. At least 8 characters long
// 2. At least one uppercase letter
// 3. At least one lowercase letter
// 4. At least one digit
// 5. No spaces allowed
// Hypothesis 1: Length check might be wrong
if (password.length() < 8)
{
return false;
}
bool hasUpper = false;
bool hasLower = false;
bool hasDigit = false;
for (char c : password)
{
// Hypothesis 2: Space check might be missing or wrong
if (c == ' ')
{
return false; // No spaces allowed
}
// Hypothesis 3: Character classification might be incorrect
if (std::isupper(c))
{
hasUpper = true;
}
else if (std::islower(c))
{
hasLower = true;
}
else if (std::isdigit(c))
{
hasDigit = true;
}
// Early exit optimization
if (hasUpper && hasLower && hasDigit)
{
break; // All requirements met
}
}
return hasUpper && hasLower && hasDigit;
}
void testHypotheses()
{
std::cout << "=== Hypothesis-Driven Debugging ===" << std::endl;
struct TestCase
{
std::string password;
bool expected;
std::string hypothesis;
};
TestCase tests[] = {
// Test Hypothesis 1: Length requirements
{"Abc123", false, "Length < 8 should be invalid"},
{"Abcdef12", true, "Length >= 8 with all requirements should be valid"},
// Test Hypothesis 2: Space handling
{"Abc 123def", false, "Spaces should make password invalid"},
{"Abc123def", true, "No spaces should be valid"},
// Test Hypothesis 3: Character classification
{"abcdefgh", false, "Only lowercase should be invalid"},
{"ABCDEFGH", false, "Only uppercase should be invalid"},
{"AbcdefGH", false, "No digits should be invalid"},
{"12345678", false, "Only digits should be invalid"},
{"Abc12345", true, "Mix of upper, lower, digits should be valid"},
// Edge cases to test additional hypotheses
{"", false, "Empty string should be invalid"},
{"A1a", false, "Too short even with all char types"},
{"Aa1!@#$%", true, "Special characters should be allowed"},
};
std::cout << "\nTesting each hypothesis:" << std::endl;
for (const auto& test : tests)
{
bool result = isValidPassword(test.password);
std::string status = (result == test.expected) ? "✓ PASS" : "✗ FAIL";
std::cout << status << " \"" << test.password << "\" -> "
<< std::boolalpha << result
<< " (" << test.hypothesis << ")" << std::endl;
if (result != test.expected)
{
std::cout << " Expected: " << test.expected
<< ", Got: " << result << std::endl;
}
}
}
// Alternative implementation to test different hypothesis
bool isValidPassword_v2(const std::string& password)
{
// Hypothesis: Original might have issues with edge cases
if (password.empty() || password.length() < 8)
{
return false;
}
int upperCount = 0, lowerCount = 0, digitCount = 0;
for (char c : password)
{
if (c == ' ') return false; // No spaces
if (c >= 'A' && c <= 'Z') upperCount++;
else if (c >= 'a' && c <= 'z') lowerCount++;
else if (c >= '0' && c <= '9') digitCount++;
}
return upperCount > 0 && lowerCount > 0 && digitCount > 0;
}
void compareImplementations()
{
std::cout << "\n=== Comparing Implementations ===" << std::endl;
std::string testPasswords[] = {
"Password123",
"password123",
"PASSWORD123",
"Password",
"Pass 123",
"Pássword123" // Test with non-ASCII character
};
for (const std::string& pwd : testPasswords)
{
bool result1 = isValidPassword(pwd);
bool result2 = isValidPassword_v2(pwd);
std::cout << "\"" << pwd << "\":" << std::endl;
std::cout << " Version 1 (std::is* functions): " << std::boolalpha << result1 << std::endl;
std::cout << " Version 2 (manual ranges): " << std::boolalpha << result2 << std::endl;
if (result1 != result2)
{
std::cout << " ⚠️ Implementations disagree!" << std::endl;
}
std::cout << std::endl;
}
}
int main()
{
testHypotheses();
compareImplementations();
return 0;
}
Rubber duck debugging enhanced
A more structured approach to explaining your code to find bugs.
Structured self-explanation
#include <iostream>
#include <vector>
// Bug: This function should merge two sorted arrays
std::vector<int> mergeSortedArrays(const std::vector<int>& arr1, const std::vector<int>& arr2)
{
std::cout << "=== Rubber Duck Debugging Session ===" << std::endl;
std::cout << "Explaining mergeSortedArrays function step by step..." << std::endl;
std::vector<int> result;
size_t i = 0, j = 0;
std::cout << "\n🦆 Duck: What are we trying to do?" << std::endl;
std::cout << "Me: Merge two sorted arrays into one sorted array." << std::endl;
std::cout << "\n🦆 Duck: How do we approach this?" << std::endl;
std::cout << "Me: Use two pointers, one for each array, and compare elements." << std::endl;
std::cout << "\n🦆 Duck: Let's trace through the main loop..." << std::endl;
while (i < arr1.size() && j < arr2.size())
{
std::cout << "Comparing arr1[" << i << "]=" << arr1[i]
<< " with arr2[" << j << "]=" << arr2[j] << std::endl;
if (arr1[i] <= arr2[j])
{
result.push_back(arr1[i]);
std::cout << "Added " << arr1[i] << " from arr1" << std::endl;
i++;
}
else
{
result.push_back(arr2[j]);
std::cout << "Added " << arr2[j] << " from arr2" << std::endl;
j++;
}
}
std::cout << "\n🦆 Duck: What happens when one array is exhausted?" << std::endl;
std::cout << "Me: We need to add remaining elements from the other array." << std::endl;
// Add remaining elements from arr1
while (i < arr1.size())
{
result.push_back(arr1[i]);
std::cout << "Added remaining " << arr1[i] << " from arr1" << std::endl;
i++;
}
// Add remaining elements from arr2
while (j < arr2.size())
{
result.push_back(arr2[j]);
std::cout << "Added remaining " << arr2[j] << " from arr2" << std::endl;
j++;
}
std::cout << "\n🦆 Duck: Does this handle all edge cases?" << std::endl;
std::cout << "Me: Let me think... empty arrays, arrays of different sizes..." << std::endl;
std::cout << "Me: Yes, the while loops handle these cases correctly." << std::endl;
return result;
}
void rubberDuckSession()
{
std::vector<int> arr1 = {1, 3, 5, 7};
std::vector<int> arr2 = {2, 4, 6, 8, 9, 10};
std::cout << "Input arrays:" << std::endl;
std::cout << "arr1: ";
for (int x : arr1) std::cout << x << " ";
std::cout << "\narr2: ";
for (int x : arr2) std::cout << x << " ";
std::cout << std::endl;
std::vector<int> merged = mergeSortedArrays(arr1, arr2);
std::cout << "\nResult: ";
for (int x : merged) std::cout << x << " ";
std::cout << std::endl;
std::cout << "\n🦆 Duck: Is the result correct?" << std::endl;
std::cout << "Me: Let me verify... 1,2,3,4,5,6,7,8,9,10 - yes, that's correct!" << std::endl;
}
int main()
{
rubberDuckSession();
return 0;
}
Performance debugging
Sometimes bugs manifest as performance problems rather than incorrect output.
Identifying performance bottlenecks
#include <iostream>
#include <vector>
#include <chrono>
#include <algorithm>
class PerformanceDebugger
{
public:
// Bug: Inefficient algorithm causing performance issues
static bool containsDuplicate_slow(const std::vector<int>& nums)
{
// O(n²) approach - performance bug for large inputs
for (size_t i = 0; i < nums.size(); ++i)
{
for (size_t j = i + 1; j < nums.size(); ++j)
{
if (nums[i] == nums[j])
{
return true;
}
}
}
return false;
}
// Fixed version with better performance
static bool containsDuplicate_fast(const std::vector<int>& nums)
{
// O(n log n) approach using sorting
std::vector<int> sorted_nums = nums;
std::sort(sorted_nums.begin(), sorted_nums.end());
for (size_t i = 1; i < sorted_nums.size(); ++i)
{
if (sorted_nums[i] == sorted_nums[i-1])
{
return true;
}
}
return false;
}
static void measurePerformance()
{
std::cout << "=== Performance Debugging ===" << std::endl;
// Test with different input sizes
std::vector<int> sizes = {100, 1000, 5000, 10000};
for (int size : sizes)
{
// Create test data
std::vector<int> testData;
for (int i = 0; i < size; ++i)
{
testData.push_back(i);
}
testData.push_back(size / 2); // Add one duplicate
std::cout << "\nTesting with " << size << " elements:" << std::endl;
// Measure slow version
auto start = std::chrono::high_resolution_clock::now();
bool result1 = containsDuplicate_slow(testData);
auto end = std::chrono::high_resolution_clock::now();
auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
// Measure fast version
start = std::chrono::high_resolution_clock::now();
bool result2 = containsDuplicate_fast(testData);
end = std::chrono::high_resolution_clock::now();
auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "Slow version: " << duration1.count() << " μs (result: "
<< std::boolalpha << result1 << ")" << std::endl;
std::cout << "Fast version: " << duration2.count() << " μs (result: "
<< std::boolalpha << result2 << ")" << std::endl;
if (duration1.count() > 0)
{
double speedup = static_cast<double>(duration1.count()) / duration2.count();
std::cout << "Speedup: " << speedup << "x faster" << std::endl;
}
}
}
};
int main()
{
PerformanceDebugger::measurePerformance();
return 0;
}
Summary
Advanced debugging tactics expand your problem-solving toolkit:
- Test-driven debugging: Create targeted tests to isolate and verify bug fixes
- Regression testing: Ensure fixes don't break existing functionality
- Code inspection: Systematic review and walkthrough of code logic
- Hypothesis-driven debugging: Form and test specific theories about bugs
- Enhanced rubber duck debugging: Structured self-explanation process
- Performance debugging: Identify and fix performance-related bugs
These techniques are particularly valuable for complex bugs that simple print debugging can't solve. They require more time and effort but can catch subtle issues that might otherwise go unnoticed.
In the next lesson, you'll learn how to use integrated debugger tools that provide even more powerful debugging capabilities.
Quiz
- What is the main advantage of test-driven debugging?
- Why are regression tests important after fixing a bug?
- How does hypothesis-driven debugging differ from random trial-and-error?
- When might performance debugging be necessary even if your program produces correct output?
- What makes structured rubber duck debugging more effective than just thinking through the problem?
Practice exercises
-
Create a test suite for this function that would catch the bug:
int binarySearch(const std::vector<int>& arr, int target) { int left = 0, right = arr.size() - 1; while (left <= right) { int mid = (left + right) / 2; if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; }
-
Write a performance test to compare these two implementations:
// Version A std::string reverseString_A(const std::string& str) { std::string result = ""; for (int i = str.length() - 1; i >= 0; --i) { result += str[i]; } return result; } // Version B std::string reverseString_B(const std::string& str) { std::string result = str; std::reverse(result.begin(), result.end()); return result; }
-
Practice hypothesis-driven debugging: Given a sorting function that sometimes produces incorrect results, what hypotheses would you test and in what order?
-
Create regression tests for a calculator class that has had bugs in the past with division by zero, integer overflow, and floating-point precision.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions