Clarification on PUBLIC/PRIVATE with target_source_group

You pretty much never want a .cpp file to be anything but PRIVATE. The only target that should be compiling the .cpp file is the target you are adding it to (there are exceptions, but those scenarios are exceedingly rare). If you add a .cpp file as PUBLIC, you are saying that it should be compiled into the current target and anything that links to it. Note that this is not talking about linking the object file produced from the .cpp file, we’re talking about compiling it. In your example, foo.cpp will be compiled as part of APP and also as part of XYZ. The object file compiled into APP will be used and the one compiled into XYZ is not used. The one compiled into APP shouldn’t be there, you only want it compiled into XYZ and linking to XYZ should then pick up that one.

For a header, the situation is considerably more involved. It has been common for IDEs to not show a header in its file lists unless the header was added to a target. Thus, it was common to see headers added directly in calls to add_executable() and add_library(). By extension, it made sense to add them using target_sources() too, but then you had to decide if PRIVATE, PUBLIC or INTERFACE was the most appropriate. Since headers don’t get compiled directly, the only real difference used to be that these keywords controlled which target(s) that file would appear under in IDEs. The usual way this played out was that a header-only library would get represented as an interface library, which in earlier versions of CMake meant you had no choice but to add headers as INTERFACE. CMake 3.19 added the ability to add headers to interface libraries as PRIVATE (or equivalently to list them in the call to add_library()), which allowed the header to be seen as part of that target in IDEs instead of the target’s consumers. For targets that were not interface libraries, headers would normally be added as PRIVATE so that those headers were associated with that target in IDE file lists.

CMake 3.23 is where things got much more interesting. That release added file sets, which fundamentally changed the recommended way to add headers to your project. Now the PRIVATE, PUBLIC and INTERFACE keywords have different impacts when used for a file set, affecting not just IDEs but also things like header search paths, installation of the headers, and verification of whether headers are self-contained. File sets brought much more powerful capabilities and meaning to headers and how they are used in a project and by consumers of that project. Fundamentally though, the high level meaning of PRIVATE, PUBLIC and INTERFACE still means something similar when defining a file set with target_sources(). PRIVATE means “only used when building this target”, INTERFACE means “only used by things that consume this target, not the target itself”, and PUBLIC merges both of those together. Thus, when you define a headers file set, you need to consider whether the headers are part of that target’s public API or not. Typically, I recommend defining two separate file sets for a target, one to contain the public headers (I often call that header set “api”) and another to contain the private headers (which I typically just call “private”). IDEs should associate headers specified in a file set with the target they are being added to by the target_sources() call in all cases, but I don’t know if they will still use the old behavior of showing non-private headers under consumers of the target.

Probably not quite what you were expecting, but hopefully that fills out a bit more of the landscape for you to explore.

2 Likes