skip to Main Content

I was reading Bjarne Stroustrups principle and practice using C++ and encountered the following in chapter 8.3 about header files:

To ease consistency checking, we #include a header both in source
files that use its declarations and in source files that provide
definitions for those declarations. That way, the compiler catches
errors as soon as possible.

There are two questions that emerged from this:

  1. How including the header file into the implementation file actually lets the compiler catch errors quicker.
  2. Thinking about question 1. I started wondering about why including header files into implementation files was necessary besides the argument of the compiler being able to catch error quicker. I get why it is logical to import a header file into another translation unit to give access to its declarations and then let the linker connect these declarations to the implementations. But how is the connection between the implementation and declarations established by the linker? Does including the header file into the implementation file tell the linker that declaration x should be connected to definition x?

I then tested a bit around to see what would happen if I did not include the header into the implementation file:

main.cpp:

#include "test_utility.h"

int main()
{
    utility::printString("hello");
}

test_utility.h:

#ifndef TEST_UTILITY
#define TEST_UTILITY

#include <string>

namespace utility
{
    void printString(const std::string& str);
}

#endif

test_utility.cpp:

#include <iostream>
#include <string>
//#include "test_utility.h"

namespace utility
{
    void printString(const std::string& str)
    {
        std::cout << str << std::endl;
    }
}

I expected a linker error from this code saying something about not finding the definition of printString(). What surprised me is that the compiler (gcc Ubuntu 11.4.0-1ubuntu1~22.04) did not give any warnings, error and the code worked fine. So what is really the point of including headers into implementation files? Is it only to help the compiler catch errors quicker like Stroustrup said? It seems the linker is able to connect corresponding implementations to corresponding definitions without the header in the implementation file.

For example does:

#include <iostream>
#include <string>
//#include "test_utility.h"

namespace utility
{
    void printString(const std::string& str, int x)
    {
        std::cout << str << std::endl;
    }
}

still generate a linker error in my testing.

Here is the CmakeLists.txt file I used for the testing:

set(PROJECT_NAME chap8)
cmake_minimum_required(VERSION 3.22)
project(${PROJECT_NAME} VERSION 1.0)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)


set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

set(TARGET_NAME test) 
add_executable(${TARGET_NAME} "${TARGET_NAME}.cpp" "test_utility.cpp")

#Compiler flags
target_compile_options(${TARGET_NAME} PRIVATE 
    -std=c++20 
    -fexceptions 
    -Wall
    -g
)

2

Answers


  1. The header file basically says "there exists a function void printString(const std::string&, int), but doesn’t give the actual implementation. It’s still enough to allow the compiler to compile code that calls this function.

    The implementation file includes the header to make sure it’s the same thing, and also to get other stuff that’s declared there.

    Compilers can even warn you if you make a global function without a preceding prototype declaration, so you don’t accidentally miss the header. For g++, this is enabled by -Wmissing-declarations (which is one of the many warnings that I’ve always enabled). Since you didn’t have that warning, your second example compiled just fine and only the linker complained because the function that you promised would exist does not in fact exist in your code.

    Also, for classes, the header file gives a lot of detail information already, so it’s absolutely required. For functions, you could get away with not including the header, but that’s not advised at all since you don’t want to unnecessarily repeat things in your code, and you want the compiler to check things sooner rather than later.
    Besides, most headers don’t just declare one function anyway — there are classes and types as well.

    Login or Signup to reply.
  2. One example that comes to my mind and justifies

    How including the header file into the implementation file actually lets the compiler catch errors quicker.

    Consider header.h:

    int printString(const std::string& str);
    

    and implementation impl.cpp

    int printString(const std::string& str) {
        std::cout << str << std::endl;
        return 0;
    }
    

    Then let’s say that somewhere in your code you are including header.h and using int res = printString("hello");. Your res is equal to 0. Then you decide to do refactoring and you change your implementation to

    void printString(const std::string& str) {
        std::cout << str << std::endl;
    }
    

    Now your program compiles and links, but this statement int res = printString("hello"); has undefined behavior. If you included header.h in your implementation, compiler would complain about

    error: ambiguating new declaration of ‘void printString(const string&)’

    As for your second question: technically it is not necessary to include the header, but try to implement something more complex than a free function, and you will see that not including the header simply doesn’t make much sense:

    header.h

    struct Foo {
        void bar();
    };
    

    impl.cpp

    // sure, you can repeat the class body, and then implement bar(), but why not just #include "header.h" ?
    struct Foo {
        void bar();
    };
    
    void Foo::bar() {
        std::cout << "yyyn";
    }
    

    Does including the header file into the implementation file tell the linker that declaration x should be connected to definition x?

    No, it only tells the compiler that printString(const std::string& str) exists, and then linker will look for the corresponding symbol (example from my gcc 11.4: _Z11printStringRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE) whenever function is called. That’s another argument that you should keep your declarations (in header) and definitions consistent.

    p.s.

    It is quite common to run into problems with 2 versions of the same library installed in your system, when your project includes headers of libraryV1 and links to libraryV2. This leads to undefined behavior that is difficult to debug.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search