-isystem on a system include directory causes errors

What's up with the following code?

#include <cmath>

int
main(int argc, char *argv[])
{
}

When compiled on a recent Arch Linux installation with GCC 6.1.1 and the flag -isystem /usr/include it produces:

$ g++ -isystem /usr/include math.cc 
In file included from math.cc:1:0:
/usr/include/c++/6.1.1/cmath:45:23: fatal error: math.h: No such file or directory
 #include_next <math.h>
                       ^
compilation terminated.

That is a very simplified example; the original command line was:

$ g++ ... -isystem `llvm-config -includedir` ...

for a part of a program using LLVM. On Arch Linux, the LLVM package is installed with its header directory in /usr/include, which is the directory reported by llvm-config. The ...'s included -Wextra and -Wconversion, which cause warnings in the LLVM headers. The -isystem flag, as opposed to -I, prevents the warnings by considering the LLVM directory to be "system headers". See the GNU C preprocessor documentation for more information.

But with an upgrade to GCC 6.1.1, the error above appears in the build.


In addition to considering the directory to contain "system headers", -isystem alters the header search list, putting the directory argument at the top of the system header directories. If the directory already exists in the search list, it is removed from its current location.

As of (at least) GCC 6.1.1, some C++ headers such as cmath use #include_next to monkey-patch C++ support for the standard C headers. See Why < cstdlib > is more complicated than you might think for more information. For example, cmath has the line:

#include_next <math.h>

#include_next, unlike the normal #include statement, starts the search for the file at the next entry in the include directory search path, rather than at the top of the search path. Since -isystem /usr/include moves /usr/include in the search path before the directory containing cmath, math.h cannot be found.

In detail, the search path for the command g++ -I /usr/include is

 /usr/include/c++/6.1.1
 /usr/include/c++/6.1.1/x86_64-pc-linux-gnu
 /usr/include/c++/6.1.1/backward
 /usr/lib/gcc/x86_64-pc-linux-gnu/6.1.1/include
 /usr/local/include
 /usr/lib/gcc/x86_64-pc-linux-gnu/6.1.1/include-fixed
 /usr/include

(/usr/include is a system directory; the -I argument does nothing.)

cmath is at the path /usr/include/c++/6.1.1/cmath, which is the first element of the search path. math.h can be found in

/usr/include/math.h
/usr/include/c++/6.1.1/math.h

The use of #include_next <math.h> in cmath ensures that the copy of math.h in /usr/include/c++/6.1.1 is skipped and that the copy used is /usr/include/math.h.

With g++ -isystem /usr/include, the search path is

 /usr/include
 /usr/include/c++/6.1.1
 /usr/include/c++/6.1.1/x86_64-pc-linux-gnu
 /usr/include/c++/6.1.1/backward
 /usr/lib/gcc/x86_64-pc-linux-gnu/6.1.1/include
 /usr/local/include
 /usr/lib/gcc/x86_64-pc-linux-gnu/6.1.1/include-fixed

The use of #include_next <math.h> now skips /usr/include/c++/6.1.1 but also /usr/include, which is above it in the search path. As a result, the compiler cannot find any copy of math.h.

To summarize, be cautious about using -isystem for its error-silencing side-effects; if the directory being included is already on the search path, the order of the path may be modified and GCC may report errors.

Something like the following Makefile work-around should suffice:

llvm.include.dir := $(shell $(LLVM_CONFIG) --includedir)
include.paths := $(shell echo | cc -v -E - 2>&1)
ifeq (,$(findstring $(llvm.include.dir),$(include.paths)))
# LLVM include directory is not in the existing paths;
# put it at the top of the system list
llvm.include := -isystem $(llvm.include.dir)
else
# LLVM include directory is already on the existing paths;
# do nothing
llvm.include :=
endif

This sets the make variable llvm.include to be either -isystem <dir> or nothing, depending on if it is actually needed or not.