DEV Community

Cover image for Why Header Files Exist: Solving C++'s Redefinition Problem
Sk
Sk

Posted on

Why Header Files Exist: Solving C++'s Redefinition Problem

Header files solve a very specific low-level problem: compiler clashes (redefinition errors) and APIs. Once you understand why compilers clash when generating binaries, the need for headers explains itself.


The Problem

Here’s a toy example (not realistic and an anti pattern), but close enough to what happens in large projects.

Suppose you have two library files: a.cpp and b.cpp.

a.cpp

float sum(float a, float b) {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

b.cpp

#include "a.cpp"

float lerp(float a, float b, float t) {
    return a * sum((1.0f - t), b * t);
}
Enter fullscreen mode Exit fullscreen mode

Now b.cpp depends on a.cpp. That’s fine.

In main.cpp you can use them like this:

#include "b.cpp"
#include <iostream>

int main() {
    float a = 1.0f, b = 3.0f, t = 0.5f;

    float result = lerp(a, b, t);
    std::cout << result << std::endl;
}
Enter fullscreen mode Exit fullscreen mode

Compile:

g++ .\main.cpp -o h.exe
Enter fullscreen mode Exit fullscreen mode

Run:

.\h.exe
Enter fullscreen mode Exit fullscreen mode

All good so far.


The Clash

But what if you also want to call sum directly in main.cpp?

#include "b.cpp"
#include "a.cpp"
#include <iostream>

int main() {
    float a = 1.0f, b = 3.0f, t = 0.5f;

    float result = lerp(a, b, t);
    std::cout << result << std::endl;

    std::cout << sum(5, 5) << std::endl;
}
Enter fullscreen mode Exit fullscreen mode

Try to compile this,

g++ .\main.cpp -o h.exe
Enter fullscreen mode Exit fullscreen mode

error:

error: redefinition of 'float sum(float, float)'
Enter fullscreen mode Exit fullscreen mode

Why? Because sum got compiled twice: once inside b.cpp, and again in main.cpp. Unlike high-level languages, C\C++ compilers don't allow this.

This is exactly one of the problems header files solve.


Header Files

There are two flavors of header files:

Modern

#pragma once
Enter fullscreen mode Exit fullscreen mode

Tells the compiler: include this file only once in this translation unit(TU) which is roughly: one .cpp file and everything it includes, no matter how many times it’s imported.

Old School

#ifndef A_H
#define A_H
// declarations
#endif
Enter fullscreen mode Exit fullscreen mode

Both do the same thing.

The key idea: a header file is a promise. It says, “this API exists, don’t worry about the implementation right now.”


Fixing Our Example

Create a.h:

#pragma once
float sum(float a, float b);
Enter fullscreen mode Exit fullscreen mode

Update a.cpp to include the header:

#include "a.h"

float sum(float a, float b) {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Now in b.cpp:

#include "a.h"  // instead of a.cpp
Enter fullscreen mode Exit fullscreen mode

And in main.cpp:

#include "a.h"  // instead of a.cpp
#include "b.cpp"
#include <iostream>
Enter fullscreen mode Exit fullscreen mode

Compile with:

g++ .\main.cpp .\a.cpp -o h.exe
Enter fullscreen mode Exit fullscreen mode

Now a.cpp is compiled once, and everything works.


Header-Only Libraries

Headers don’t have to just declare APIs, they can also contain full implementations. That’s why “header-only” libraries exist.

Example a.h:

#pragma once

float sum(float a, float b) {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Since #pragma once prevents duplicate inclusion in a TU, this works fine too.

When should you do this? It’s up to you. For safety, most people keep headers as pure API declarations and put real implementations in .cpp files. That way headers act as a shadow, not the actual code.


Wrap Up

Headers:

  • Prevent multiple-definition clashes.
  • Define APIs that promise “this exists, trust me.”
  • Separate declarations from implementations (good practice).
  • Can even hold full implementations in header-only libs.

Once you see it, it clicks: header files are the glue that keeps large C++ projects sane.

Top comments (0)