Coming Soon

This lesson is currently being developed

Assert and static_assert

Use assertions for debugging and compile-time checks.

Error Detection and Handling
Chapter
Beginner
Difficulty
35min
Estimated Time

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

In Progress

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

  1. What's the difference between assert() and static_assert?
  2. When should you use assertions versus regular error handling?
  3. What happens to assert() statements when NDEBUG is defined?
  4. Why shouldn't you put function calls with side effects in assertions?
  5. What are invariants and how do assertions help verify them?

Practice exercises

Add appropriate assertions to these functions:

  1. Create a matrix class with assertion checks for valid indices and dimensions
  2. Write a stack implementation with assertions for overflow/underflow conditions
  3. Build a date class with static_assert and runtime assertions for valid dates
  4. Implement a safe string class with comprehensive assertion checking

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion