Coming Soon
This lesson is currently being developed
Assert and static_assert
Use assertions for debugging and compile-time checks.
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.
9.6 — Assert and static_assert
In this lesson, you'll learn about assertions - debugging tools that help catch programming errors by checking conditions and stopping program execution when assumptions are violated.
What are assertions?
Assertions are statements that check whether a condition is true at a specific point in your program. If the condition is false, the assertion fails and the program terminates with an error message. Think of assertions like safety checks in an elevator - they ensure critical conditions are met before proceeding.
Assertions are primarily debugging tools that help you:
- Catch programming errors during development
- Document assumptions in your code
- Verify preconditions and postconditions
- Find bugs early before they cause worse problems
Runtime assertions with assert()
The assert()
macro checks conditions at runtime and terminates the program if they fail:
#include <iostream>
#include <cassert>
int divide(int numerator, int denominator)
{
// Assert that denominator is not zero
assert(denominator != 0);
return numerator / denominator;
}
int factorial(int n)
{
// Assert precondition: n must be non-negative
assert(n >= 0);
if (n <= 1)
return 1;
int result = 1;
for (int i = 2; i <= n; ++i)
{
result *= i;
}
// Assert postcondition: result should be positive
assert(result > 0);
return result;
}
int main()
{
std::cout << "Testing assertions:" << std::endl;
// These work fine
std::cout << "10 / 2 = " << divide(10, 2) << std::endl;
std::cout << "5! = " << factorial(5) << std::endl;
// This will trigger an assertion failure
std::cout << "Attempting division by zero..." << std::endl;
// divide(10, 0); // Would terminate the program with assertion failure
std::cout << "Program completed successfully" << std::endl;
return 0;
}
Output:
Testing assertions:
10 / 2 = 5
5! = 120
Attempting division by zero...
Program completed successfully
If you uncommented the divide(10, 0)
line, the program would terminate with an error like:
Assertion failed: (denominator != 0), function divide, file program.cpp, line 6.
Using assertions for debugging
Checking array bounds
#include <iostream>
#include <cassert>
class SafeArray
{
private:
static const int MAX_SIZE = 10;
int data[MAX_SIZE];
int currentSize;
public:
SafeArray() : currentSize(0) {}
void push_back(int value)
{
// Assert we don't exceed capacity
assert(currentSize < MAX_SIZE);
data[currentSize] = value;
currentSize++;
// Assert our size is consistent
assert(currentSize <= MAX_SIZE);
assert(currentSize > 0);
}
int get(int index) const
{
// Assert valid index
assert(index >= 0);
assert(index < currentSize);
return data[index];
}
int size() const
{
// Assert size is always valid
assert(currentSize >= 0);
assert(currentSize <= MAX_SIZE);
return currentSize;
}
void clear()
{
currentSize = 0;
// Assert we're in a clean state
assert(currentSize == 0);
}
};
int main()
{
SafeArray arr;
// Add some elements
for (int i = 1; i <= 5; ++i)
{
arr.push_back(i * 10);
std::cout << "Added " << (i * 10) << ", size now: " << arr.size() << std::endl;
}
// Access elements safely
for (int i = 0; i < arr.size(); ++i)
{
std::cout << "arr[" << i << "] = " << arr.get(i) << std::endl;
}
// These would trigger assertion failures:
// arr.get(-1); // Negative index
// arr.get(10); // Index too large
std::cout << "All assertions passed!" << std::endl;
return 0;
}
Output:
Added 10, size now: 1
Added 20, size now: 2
Added 30, size now: 3
Added 40, size now: 4
Added 50, size now: 5
arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50
All assertions passed!
Checking invariants
#include <iostream>
#include <cassert>
class BankAccount
{
private:
double balance;
int accountId;
bool isOpen;
void checkInvariants() const
{
// Account invariants that should always be true
assert(accountId > 0); // Valid account ID
assert(balance >= 0.0); // Non-negative balance
assert(isOpen || balance == 0.0); // Closed accounts must have zero balance
}
public:
BankAccount(int id, double initialBalance)
: accountId(id), balance(initialBalance), isOpen(true)
{
// Assert constructor preconditions
assert(id > 0);
assert(initialBalance >= 0.0);
checkInvariants();
}
void deposit(double amount)
{
// Assert preconditions
assert(amount > 0.0);
assert(isOpen);
checkInvariants(); // Check state before operation
balance += amount;
checkInvariants(); // Check state after operation
}
void withdraw(double amount)
{
// Assert preconditions
assert(amount > 0.0);
assert(isOpen);
assert(amount <= balance); // Sufficient funds
checkInvariants();
balance -= amount;
checkInvariants();
}
void closeAccount()
{
assert(isOpen);
assert(balance == 0.0); // Can only close with zero balance
isOpen = false;
checkInvariants();
}
double getBalance() const
{
checkInvariants();
return balance;
}
bool isAccountOpen() const
{
checkInvariants();
return isOpen;
}
};
int main()
{
BankAccount account(12345, 1000.0);
std::cout << "Initial balance: $" << account.getBalance() << std::endl;
account.deposit(200.0);
std::cout << "After deposit: $" << account.getBalance() << std::endl;
account.withdraw(300.0);
std::cout << "After withdrawal: $" << account.getBalance() << std::endl;
// These would trigger assertion failures:
// account.deposit(-50.0); // Negative deposit
// account.withdraw(2000.0); // Insufficient funds
// account.closeAccount(); // Non-zero balance
std::cout << "All account operations passed assertions!" << std::endl;
return 0;
}
Output:
Initial balance: $1000
After deposit: $1200
After withdrawal: $900
All account operations passed assertions!
Compile-time assertions with static_assert
static_assert
checks conditions at compile time, catching errors before the program even runs:
#include <iostream>
#include <type_traits>
// Check fundamental assumptions at compile time
static_assert(sizeof(int) >= 4, "This program requires int to be at least 4 bytes");
static_assert(sizeof(void*) == sizeof(size_t), "Pointer and size_t must be same size");
template<int N>
class FixedArray
{
// Ensure template parameter makes sense
static_assert(N > 0, "Array size must be positive");
static_assert(N <= 1000, "Array size too large (max 1000)");
private:
int data[N];
public:
constexpr int size() const { return N; }
int& operator[](int index)
{
// Runtime check for debug builds
assert(index >= 0 && index < N);
return data[index];
}
};
template<typename T>
void processNumericType(T value)
{
// Ensure T is a numeric type
static_assert(std::is_arithmetic<T>::value, "T must be a numeric type");
std::cout << "Processing numeric value: " << value << std::endl;
}
// Function that only works with specific type sizes
void processByte(unsigned char byte)
{
static_assert(sizeof(unsigned char) == 1, "unsigned char must be exactly 1 byte");
std::cout << "Processing byte: " << static_cast<int>(byte) << std::endl;
}
constexpr int calculateFactorial(int n)
{
// Ensure compile-time calculation limits
static_assert(n >= 0, "Factorial input must be non-negative");
static_assert(n <= 12, "Factorial too large for compile-time calculation");
return (n <= 1) ? 1 : n * calculateFactorial(n - 1);
}
int main()
{
std::cout << "Compile-time assertions passed!" << std::endl;
// Template instantiation with valid size
FixedArray<5> arr;
arr[0] = 42;
std::cout << "Array size: " << arr.size() << ", first element: " << arr[0] << std::endl;
// These would cause compile-time errors:
// FixedArray<0> invalidArr1; // Size not positive
// FixedArray<2000> invalidArr2; // Size too large
// Function calls with valid types
processNumericType(42);
processNumericType(3.14);
// processNumericType("hello"); // Compile error: not numeric
processByte(255);
// Compile-time constant calculation
constexpr int fact5 = calculateFactorial(5);
std::cout << "5! = " << fact5 << std::endl;
return 0;
}
Output:
Compile-time assertions passed!
Array size: 5, first element: 42
Processing numeric value: 42
Processing numeric value: 3.14
Processing byte: 255
5! = 120
If any static_assert
condition fails, the program won't compile and you'll get an error message with your custom text.
Conditional compilation and NDEBUG
Assertions can be disabled in release builds using the NDEBUG macro:
#include <iostream>
#include <cassert>
// This shows how assertions behave in different build modes
void demonstrateAssertBehavior()
{
int x = 5;
std::cout << "Value of x: " << x << std::endl;
// This assertion will be active in debug builds, disabled in release
assert(x > 0 && "x should be positive");
std::cout << "After assertion check" << std::endl;
#ifdef NDEBUG
std::cout << "Running in RELEASE mode - assertions disabled" << std::endl;
#else
std::cout << "Running in DEBUG mode - assertions enabled" << std::endl;
#endif
}
// Custom assertion macro that's always active
#define ALWAYS_ASSERT(condition, message) \
do { \
if (!(condition)) { \
std::cerr << "Assertion failed: " << message << std::endl; \
std::cerr << "File: " << __FILE__ << ", Line: " << __LINE__ << std::endl; \
std::abort(); \
} \
} while (0)
void criticalFunction(int* ptr)
{
// Use regular assert for debug-only checks
assert(ptr != nullptr);
// Use always-active assertion for critical checks
ALWAYS_ASSERT(ptr != nullptr, "Critical: null pointer passed to criticalFunction");
std::cout << "Processing value: " << *ptr << std::endl;
}
int main()
{
demonstrateAssertBehavior();
int value = 42;
criticalFunction(&value);
// This would trigger both assertions:
// criticalFunction(nullptr);
return 0;
}
Debug Output:
Value of x: 5
After assertion check
Running in DEBUG mode - assertions enabled
Processing value: 42
Release Output (compiled with -DNDEBUG):
Value of x: 5
After assertion check
Running in RELEASE mode - assertions disabled
Processing value: 42
Best practices for assertions
1. Use assertions for programming errors, not user errors
#include <iostream>
#include <cassert>
// Good: Assert programming assumptions
int arraySum(int arr[], int size)
{
// Programming error - caller should ensure valid parameters
assert(arr != nullptr);
assert(size >= 0);
int sum = 0;
for (int i = 0; i < size; ++i)
{
sum += arr[i];
}
return sum;
}
// Good: Handle user errors gracefully
bool getValidAge(int& age)
{
std::cout << "Enter your age: ";
std::cin >> age;
// User error - handle gracefully, don't assert
if (age < 0 || age > 150)
{
std::cout << "Invalid age. Please enter age between 0 and 150." << std::endl;
return false;
}
return true;
}
int main()
{
int numbers[] = {1, 2, 3, 4, 5};
int sum = arraySum(numbers, 5); // Valid call
std::cout << "Sum: " << sum << std::endl;
// Don't do this - it's a programming error:
// arraySum(nullptr, 5); // Would trigger assertion
int age;
if (getValidAge(age))
{
std::cout << "Your age is: " << age << std::endl;
}
return 0;
}
2. Write clear assertion messages
#include <cassert>
#include <iostream>
void processBuffer(char* buffer, int size, int maxSize)
{
// Poor: No message
assert(buffer);
assert(size > 0);
assert(maxSize > 0);
// Better: Descriptive messages
assert(buffer != nullptr && "Buffer pointer cannot be null");
assert(size > 0 && "Buffer size must be positive");
assert(maxSize > 0 && "Maximum buffer size must be positive");
assert(size <= maxSize && "Buffer size cannot exceed maximum size");
std::cout << "Processing " << size << " bytes" << std::endl;
// Process buffer...
for (int i = 0; i < size; ++i)
{
// Assert we're within bounds
assert(i < maxSize && "Processing index exceeds maximum buffer size");
buffer[i] = 'X'; // Dummy processing
}
}
int main()
{
char buffer[100];
processBuffer(buffer, 50, 100);
std::cout << "Buffer processed successfully" << std::endl;
return 0;
}
3. Use static_assert for compile-time requirements
#include <iostream>
#include <type_traits>
// Check platform requirements
static_assert(sizeof(int) == 4, "This code assumes 32-bit integers");
static_assert(sizeof(long long) == 8, "This code requires 64-bit long long");
template<typename T>
class NumericWrapper
{
// Ensure template parameter is appropriate
static_assert(std::is_arithmetic<T>::value, "NumericWrapper requires numeric type");
static_assert(!std::is_same<T, bool>::value, "NumericWrapper doesn't support bool");
private:
T value;
public:
NumericWrapper(T val) : value(val) {}
T getValue() const { return value; }
void setValue(T val)
{
// Runtime assertion for value constraints
assert(val >= std::numeric_limits<T>::min());
assert(val <= std::numeric_limits<T>::max());
value = val;
}
};
// Configuration constants with compile-time validation
constexpr int MAX_USERS = 1000;
constexpr int MAX_CONNECTIONS = 100;
static_assert(MAX_USERS > 0, "Maximum users must be positive");
static_assert(MAX_CONNECTIONS > 0, "Maximum connections must be positive");
static_assert(MAX_CONNECTIONS <= MAX_USERS, "Connections cannot exceed user limit");
int main()
{
NumericWrapper<int> intWrapper(42);
std::cout << "Integer value: " << intWrapper.getValue() << std::endl;
NumericWrapper<double> doubleWrapper(3.14);
std::cout << "Double value: " << doubleWrapper.getValue() << std::endl;
// These would cause compile errors:
// NumericWrapper<std::string> stringWrapper("hello"); // Not numeric
// NumericWrapper<bool> boolWrapper(true); // Explicitly excluded
std::cout << "Configuration: " << MAX_USERS << " users, " << MAX_CONNECTIONS << " connections" << std::endl;
return 0;
}
When not to use assertions
Don't assert for expected runtime conditions
#include <iostream>
#include <fstream>
// Bad: Using assertion for file operations
void badFileReader(const std::string& filename)
{
std::ifstream file(filename);
// Bad: File might not exist - this is expected runtime behavior
// assert(file.is_open() && "File should open");
// Good: Handle the error properly
if (!file.is_open())
{
std::cout << "Error: Could not open file '" << filename << "'" << std::endl;
return;
}
std::cout << "File opened successfully" << std::endl;
file.close();
}
// Bad: Using assertion for user input validation
void badInputValidator()
{
int age;
std::cout << "Enter age: ";
std::cin >> age;
// Bad: User input errors are expected
// assert(age >= 0 && age <= 150 && "Age should be reasonable");
// Good: Validate and handle gracefully
if (age < 0 || age > 150)
{
std::cout << "Please enter a valid age between 0 and 150" << std::endl;
return;
}
std::cout << "Age accepted: " << age << std::endl;
}
int main()
{
badFileReader("existing_file.txt");
badFileReader("nonexistent_file.txt");
badInputValidator();
return 0;
}
Don't assert conditions with side effects
#include <iostream>
#include <cassert>
int globalCounter = 0;
int incrementAndReturn()
{
globalCounter++;
return globalCounter;
}
void demonstrateSideEffects()
{
// Bad: Function call in assertion might not execute in release builds
// assert(incrementAndReturn() > 0);
// Good: Separate the side effect from the assertion
int result = incrementAndReturn();
assert(result > 0 && "Counter should be positive");
std::cout << "Counter: " << globalCounter << std::endl;
}
int main()
{
demonstrateSideEffects();
return 0;
}
Summary
Assertions are powerful debugging tools that help catch programming errors:
assert() macro:
- Checks conditions at runtime
- Terminates program if condition fails
- Can be disabled in release builds with NDEBUG
- Good for checking preconditions, postconditions, and invariants
static_assert:
- Checks conditions at compile time
- Prevents compilation if condition fails
- Cannot be disabled
- Good for checking template parameters and platform assumptions
Best practices:
- Use assertions for programming errors, not user errors
- Write clear, descriptive assertion messages
- Don't put side effects in assertion conditions
- Use static_assert for compile-time requirements
- Check invariants, preconditions, and postconditions
- Consider custom assertion macros for always-active checks
Assertions help you catch bugs early and document assumptions in your code, making your programs more reliable and easier to debug.
Quiz
- What's the difference between
assert()
andstatic_assert
? - When should you use assertions versus regular error handling?
- What happens to
assert()
statements when NDEBUG is defined? - Why shouldn't you put function calls with side effects in assertions?
- What are invariants and how do assertions help verify them?
Practice exercises
Add appropriate assertions to these functions:
- Create a matrix class with assertion checks for valid indices and dimensions
- Write a stack implementation with assertions for overflow/underflow conditions
- Build a date class with static_assert and runtime assertions for valid dates
- Implement a safe string class with comprehensive assertion checking
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions