Pitfalls of the include_directories function

Hi guys,

I am currently working as part of a team porting a ~20 year old project from a pure Makefile based system to using CMake.

We are mostly complete, however I have been having a sometimes intense debate with one of my
senior colleagues about the dangers of using the include_directories function as opposed to the preferred target_include_directories function.

To be clear here, the project we’re working on was already in a poor state (builds took hours to run), the old Makefile system would copy all project header files into a new directory defined as ${DERBASE}/include and then that directory would be appended to the compiler’s flags as CC_FLAGS=-I${DERBASE}/include, each library would have its own directory within this global include directory.

We have got to the point where each project library is defined as a target and each target is currently using the target_include_directories function.

Unfortunately, there are problems that we just do not have time to fix, one of the big ones of which is the prevalence of circular dependencies between shared libraries.

This has led to us having to work around the poor code structure using generator expressions like target_property:<target>,INTERFACE_INCLUDE_DIRECTORIES> to resolve header file dependencies and dealing with the link time dependencies downstream.

My senior colleague is currently pushing for us to remove the calls to target_include_directories and revert back to a “global” include directory which would be attached to every target via the include_directories function because my colleague is concerned about other developers being confused by the use of generator expressions, they also have evidence of a performance improvement in build times.

Currently our builds take ~30 minutes, and the performance improvement we’re seeing is between roughly 25-33%, so build times are down to between 20 - 25 minutes.

My fear is that we could be setting ourselves up for a big problem down the line by deviating from what is now considered more standardised practice, at the very least there is documentation we are able to refer devs to with regards to generator expressions.

I should note that I have gathered evidence that by changing compilers and linkers and their settings we can reduce our build times to <6 minutes, which indicates to me that include directories are not the source of our slower build times and it has got more to do with the number of link dependencies downstream targets have owing to circular dependencies found between upstream targets

I am currently trying to collate evidence to help show my colleague(s) the impact of this choice could have in the future.

The first reference I have is directly from the include_directories documentation:

Prefer the target_include_directories() command to add include directories to individual targets and optionally propagate/export them to dependents.

I have also been able to find a couple of blog posts and a video (CppCon 2017, Mathieu Ropert)

Unfortunately I cannot add any further links as a new user.

If possible, could anyone please point me to any resources on why the target_include_directories function should be preferred of the include_directories function, my primary reasoning is that we break down our project into granular targets with well defined interfaces which should then allow us to come back and resolve the circular dependency issues at a later date (the big ol’ TODO list we have) however I am still looking for hard evidence to help explain to my colleague the potential consequences of the performance improvement they believe they have found without us having to discover them for ourselves in a few months time.

If anyone can point me to further reading beyond what I have been able to provide above it would be most appreciated, I just need pointers if anyone happens to know of anywhere I have not found.

Please also provide any evidence of the include_directories function being of use as I am well aware that there could very well be a place for it!

Thanks!

Once the circular dependencies are figured out, I think that the use of genexes here should be gone.

I agree here. The problem is that include_directories are directory scoped and considered private, so if you have X that references Y in its headers and Y’s headers are offered to X via include_directories, anything using X would need to know to also add Y even if it is purely transitive. Adding the include directory to Y directly lets X do target_link_libraries(X PUBLIC Y) and anything doing target_link_libraries(Any PRIVATE X) would get Y’s include directories automatically.