Classic C++ problem with #include directives

Posted on . Updated on .

A few days ago we had a perplexing bug in my day job that turned out to be related to a classic C++ problem with #include directives. Hopefully, C++ modules will solve part of the mess that are #include directives, but no official specification exists yet and they didn’t make it into C++17.

One of the classic questions of #include directives is: what is the difference between an #include directive using double quotes and one using angles? The C standard mentions headers using angles may not be implemented as actual files. For example, there’s no need for a cstdio file to exist when someone uses #include <cstdio>. Double quotes, OTOH, refer to files, and in both cases the implementation is free to choose how to do a lot of things. In practice, different compilers do things differently. From now on, I’ll be talking about Unix systems and GCC/Clang.

GCC and Clang both do a very simple thing. Most standard headers (every single one?) are actual files that exist on disk. The difference between double quotes and angles is that double quotes make the preprocessor look for the file first in the same directory the source file including it is located. If not found, with both angles and double quotes, the compiler proceeds to search for those headers in the list of directories passed using the -I flag. If it’s still not found, a list of standard system directories is used.

Combining the theory and practice above, some people use angles for every standard or system header, and quotes for every “user header” that belongs to the project they’re building, even if the included header is not in the same directory as the source file. Other people like myself prefer to use double quotes when the file is actually in the same directory as the source file including it, probably meaning the header belongs to the same module. Headers belonging to other modules, even if local to the project, are included using angles and passing the corresponding -I option to the compiler that would be needed in any case. Of course, there’s people that don’t fall inside of either camp.

A second typical questions would be: is there any difference between using angles and passing the current directory (dot) using -I., and using quotes without passing the -I option? Yes there is, when the file you’re compiling is not in the directory you’re calling the compiler from. In that case, angles combined with -I. will search for the header in the directory you’re calling the compiler from (the current working directory), while double quotes will make the preprocessor search for the header in the source file directory. For example, imagine the following hierarchy:

header.h
subdir/main.c
subdir/header.h

When compiling subdir/main.c from the base directory, if subdir/main.c uses #include <header.h> and you pass the -I. option, header.h, in the base directory, will be included. If subdir/main.c uses #include "header.h", subdir/header.h will be included instead.

Subtle but important. A small part of our build system used an auxiliary tool that generated a temporary file in /tmp, and ran the preprocessor on it. When that temporary file used #include directives with double quotes, the included file was searched for in /tmp first. Normally, it didn’t exist there. But one day it did because someone had put a temporary copy in /tmp to do some tests. The included file had outdated contents in /tmp and, furthermore, everything happened to compile and build just fine. Problems only appeared in runtime and it was very hard to find out what was happening because the original source code didn’t match what was being compiled into the binary. It took us a whole day and the solution, in our case, involved using a temporary directory inside /tmp instead of creating the temporary files directly there.

Load comments