8

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.

Tommy McGuire
  • 1,223
  • 13
  • 16

1 Answers1

13

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.

Tommy McGuire
  • 1,223
  • 13
  • 16
  • I don't understand "the use of #include_next 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." Is this implying that the list of search paths is searched from bottom to top? That is, why would this skip `/usr/include/c++/6.1.1/math.h` instead of `/usr/include/math.h`? I might be confused by this list "math.h can be found in /usr/include/math.h /usr/include/c++/6.1.1/math.h ", as those are not the order in which they would be found? – David Doria Dec 08 '16 at 14:08
  • Does anyone know how to fix this generally in CMake? It seems like sometimes `find_package` calls will end up setting system include paths that will produce exactly the same type of problem as the OP reported. – David Doria Dec 08 '16 at 17:16
  • The gcc-specific directive `#include_next` behaves in a special way; see the gcc info docs. The search path isn't processed from bottom to top, but the search starts after the directory the current file was found in. Check the Red Hat developers blog post for the reasoning. – Tommy McGuire Dec 09 '16 at 23:52
  • 1
    I don't know any thing about CMake, but the (mis-) feature is invoked by the -isystem gcc argument; the work-around is to ensure that -isystem *is not* added to the command line if the directory is already in the system search path. – Tommy McGuire Dec 09 '16 at 23:54
  • I get it now. Do you think I can update the answer to: https://gist.github.com/daviddoria/0e28d5d39b3a59afaec5f892e67f27c5 Note the primary difference is the order in which the two paths that math.h would be found is switched so that they are written in the same order that they would be found. – David Doria Dec 10 '16 at 02:40