C - Design by Contract in C

Design by Contract (DbC) is a software development methodology that defines clear agreements, or "contracts," between different parts of a program. These contracts specify what a function expects before it executes, what it guarantees after execution, and what conditions must always remain true throughout the program. Although the C programming language does not provide built-in support for Design by Contract, developers can implement its principles using assertions, conditional checks, and well-documented function interfaces.

Applying Design by Contract helps developers create more reliable, maintainable, and predictable software. By explicitly stating the responsibilities of both the caller and the function, programmers can detect errors early and reduce unexpected behavior during execution.

What Is a Contract?

A contract in programming is an agreement between a function and its caller. It consists of three major components:

Preconditions

Preconditions define the conditions that must be true before a function begins execution. The caller is responsible for satisfying these conditions.

For example:

  • A pointer passed to a function should not be NULL.

  • An array index should be within valid limits.

  • A divisor should not be zero.

  • Memory should already be allocated before accessing it.

Example:

#include <assert.h>

int divide(int a, int b)
{
    assert(b != 0);
    return a / b;
}

Here, the function assumes that the denominator is not zero. If the caller passes zero, the assertion fails, immediately indicating incorrect usage.

Postconditions

Postconditions describe what the function guarantees after it completes successfully.

For example:

  • The returned pointer is valid.

  • The output array is sorted.

  • A file has been successfully written.

  • The function returns a positive value.

Example:

#include <assert.h>

int square(int x)
{
    int result = x * x;
    assert(result >= 0);
    return result;
}

The function guarantees that the returned value is never negative for valid integer inputs.

Invariants

An invariant is a condition that always remains true while the program or object exists.

Although C is not object-oriented, invariants are useful when working with structures, linked lists, queues, stacks, and other data structures.

Example:

struct Stack
{
    int top;
    int size;
};

void checkStack(struct Stack *s)
{
    assert(s->top >= -1);
    assert(s->top < s->size);
}

These conditions should always remain true regardless of the operations performed on the stack.

Why Use Design by Contract?

Design by Contract improves software quality in several ways.

Early Error Detection

Incorrect function usage is detected immediately rather than producing unpredictable behavior later.

Better Documentation

The contract itself explains how the function should be used, reducing confusion among developers.

Easier Debugging

Instead of searching through multiple files for the cause of a bug, assertions identify the exact location where a contract is violated.

Improved Reliability

Functions become more dependable because invalid input is detected before processing begins.

Easier Maintenance

Future developers can understand the expected behavior of functions by reading their contracts.

Implementing Preconditions

One common way to implement preconditions is using the assert() macro.

Example:

#include <assert.h>

void printArray(int arr[], int size)
{
    assert(arr != NULL);
    assert(size > 0);

    for(int i = 0; i < size; i++)
        printf("%d ", arr[i]);
}

The function assumes:

  • The array exists.

  • The size is greater than zero.

If either condition is false, the program stops immediately during debugging.

Using Conditional Statements Instead of Assertions

Assertions are mainly intended for debugging. In production software, it is often better to validate inputs using conditional statements.

Example:

int divide(int a, int b)
{
    if(b == 0)
    {
        printf("Division by zero is not allowed.\n");
        return 0;
    }

    return a / b;
}

Unlike assertions, this approach allows the program to continue running by handling the error gracefully.

Combining Assertions and Error Handling

Many professional applications use both assertions and runtime error handling.

Example:

#include <assert.h>

FILE *openFile(const char *filename)
{
    assert(filename != NULL);

    FILE *fp = fopen(filename, "r");

    if(fp == NULL)
    {
        printf("Unable to open file.\n");
    }

    return fp;
}

Here:

  • Assertion verifies programmer errors.

  • Runtime checks handle user-related errors.

This combination provides robust and maintainable code.

Documenting Contracts

Even when assertions are not used, documenting contracts improves code readability.

Example:

/*
Precondition:
    number must be greater than zero

Postcondition:
    Returns factorial of the number
*/

int factorial(int number)
{
    int result = 1;

    for(int i = 1; i <= number; i++)
        result *= i;

    return result;
}

Any developer reading this function immediately understands its expected usage.

Contract Example in Array Processing

#include <stdio.h>
#include <assert.h>

int findMaximum(int arr[], int size)
{
    assert(arr != NULL);
    assert(size > 0);

    int max = arr[0];

    for(int i = 1; i < size; i++)
    {
        if(arr[i] > max)
            max = arr[i];
    }

    assert(max >= arr[0]);

    return max;
}

int main()
{
    int numbers[] = {15, 28, 9, 42, 17};

    printf("Maximum = %d\n", findMaximum(numbers, 5));

    return 0;
}

Contract Used

Preconditions:

  • The array pointer is valid.

  • The array contains at least one element.

Postcondition:

  • The returned value is the largest element in the array.

Using Contracts in Data Structures

Consider a queue implementation.

struct Queue
{
    int front;
    int rear;
    int capacity;
};

Invariant:

front <= rear
rear < capacity
front >= 0

Every enqueue and dequeue operation should preserve these conditions.

Assertions Can Be Disabled

The assert() macro only works when debugging.

If the macro NDEBUG is defined before including assert.h, all assertions are ignored.

Example:

#define NDEBUG
#include <assert.h>

This allows production programs to run without the performance overhead of assertions.

Best Practices for Design by Contract

  • Clearly define the responsibilities of every function.

  • Validate input parameters before processing.

  • Use assertions to detect programming mistakes during development.

  • Use runtime error handling for recoverable errors.

  • Document preconditions and postconditions in comments.

  • Keep contracts simple and easy to understand.

  • Check critical assumptions whenever data is modified.

  • Test functions with both valid and invalid inputs to ensure contracts are enforced.

Advantages

  • Improves program correctness.

  • Detects bugs at an early stage.

  • Simplifies debugging.

  • Makes code easier to understand.

  • Encourages modular programming.

  • Provides clear expectations for function behavior.

  • Reduces maintenance effort.

  • Improves software reliability.

Limitations

  • C does not provide built-in language support for Design by Contract.

  • Assertions are primarily intended for debugging and may be disabled in release builds.

  • Excessive use of assertions can make code harder to read if not applied thoughtfully.

  • Developers must manually define and maintain contracts.

  • Runtime checks can introduce slight performance overhead in critical applications.

Conclusion

Design by Contract is a disciplined programming approach that establishes clear expectations between functions and their callers. In C, it is implemented through assertions, input validation, documentation, and consistency checks rather than dedicated language features. By defining preconditions, postconditions, and invariants, developers can detect errors early, improve software reliability, simplify debugging, and create code that is easier to maintain. Although it requires careful planning and manual implementation, Design by Contract is a valuable practice for developing robust and high-quality C applications, particularly in systems programming, embedded software, and large-scale projects.