Construct path for #include directive with macro
Asked Answered
S

4

16

I would like to have include file paths dynamically created by a macro for a target-configuration-dependent part of my program.

for example, I would like to construct a macro that would be invoked like this:

#include TARGET_PATH_OF(header.h)

Which will expand to a something like this:

#include "corefoundation/header.h"

when the source is configured (in this case) for OSX

So far all attempts have failed. I'm hoping someone out there has done this before?

example of what does not work:

#include <iostream>
#include <boost/preprocessor.hpp>

#define Dir directory/
#define File filename.h

#define MakePath(f) BOOST_PP_STRINGIZE(BOOST_PP_CAT(Dir,f))
#define MyPath MakePath(File)

using namespace std;

int main() {
    // this is a test - yes I know I could just concatenate strings here
    // but that is not the case for #include
    cout << MyPath << endl;
}

errors:

./enableif.cpp:31:13: error: pasting formed '/filename', an invalid preprocessing token
    cout << MyPath << endl;
            ^
./enableif.cpp:26:16: note: expanded from macro 'MyPath'
#define MyPath MakePath(File)
               ^
./enableif.cpp:25:40: note: expanded from macro 'MakePath'
#define MakePath(f) BOOST_PP_STRINGIZE(BOOST_PP_CAT(Dir,f))
                                       ^
/usr/local/include/boost/preprocessor/cat.hpp:22:32: note: expanded from macro 'BOOST_PP_CAT'
#    define BOOST_PP_CAT(a, b) BOOST_PP_CAT_I(a, b)
                               ^
/usr/local/include/boost/preprocessor/cat.hpp:29:36: note: expanded from macro 'BOOST_PP_CAT_I'
#    define BOOST_PP_CAT_I(a, b) a ## b
                                   ^
1 error generated.
Snider answered 18/8, 2015 at 7:30 Comment(1)
Related: #3179446Jamilla
G
20

I tend to agree with the comment in utnapistim's answer that you shouldn't do this even though you can. But, in fact, you can, with standard-conformant C compilers. [Note 1]

There are two issues to overcome. The first one is that you cannot use the ## operator to create something which is not a valid preprocessor token, and pathnames do not qualify as valid preprocessor tokens because they include / and . characters. (The . would be ok if the token started with a digit, but the / will never work.)

You don't actually need to concatenate tokens in order to stringify them with the # operator, since that operator will stringify an entire macro argument, and the argument may consist of multiple tokens. However, stringify respects whitespace [Note 2], so STRINGIFY(Dir File) won't work; it will result in "directory/ filename.h" and the extraneous space in the filename will cause the #include to fail. So you need to concate Dir and File without any whitespace.

The following solves the second problem by using a function-like macro which just returns its argument:

#define IDENT(x) x
#define XSTR(x) #x
#define STR(x) XSTR(x)
#define PATH(x,y) STR(IDENT(x)IDENT(y))
 
#define Dir sys/
#define File socket.h

#include PATH(Dir,File)

Warning: (Thanks to @jed for passing on this issue.) If the strings being concatenated contain identifiers which are defined elsewhere as macros, then unexpected macro substitution will occur here. Caution should be taken to avoid this scenario, particularly if Dir and/or File are not controlled (for example, by being defined as a command-line parameter in the compiler invocation).

You need to also be aware than some implementations may define words which are likely to show up in a token-like fashion in a file path. For example, GCC may define macros with names like unix and linux unless it is invoked with an explicit C standard (which is not the default). That could be triggered by paths like platform/linux/my-header.h or even linux-specific/my-header.h.

To avoid these issues, I'd recommend that if you use this hack:

  • you use a C (or C11) standards-conformant compiler setting, and

  • you place the sequence very early in your source file, ideally before including any other header, or at least any header outside of the standard library.

Also, you wouldn't need the complication of the IDENT macro if you could write the concatenation without spaces. For example:

#define XSTR(x) #x
#define STR(x) XSTR(x)

#define Dir sys
#define File socket.h

#include STR(Dir/File)

Notes

  1. I tried it with clang, gcc and icc, as available on godbolt. I don't know if it works with Visual Studio.

  2. More accurately, it semi-respects whitespace: whitespace is converted to a single space character.

Giffard answered 18/8, 2015 at 16:10 Comment(11)
awesome. thank you for putting the time in on this. I'm using clang and gcc for the time-being. windows_mobile will be the last hurdle, and probably the highest, in many areas, since we're using c++14.Snider
Thanks for this.. My situation was explicitly where I could not use the include path values due to collisions. Until I can clean things up this will make my life a bit more manageable.Guss
Note the side-effects of expansion being performed within paths. For example #include PATH(linux/,userfaultfd.h) fails with gnu C/C++ dialects (default with gcc and clang) because they define linux=1, resulting an attempt to include "1/userfaultfd.h". There are similar problems constructing a path like foo-unix/bar.h (since unix=1 is also defined). You'll want to be sure that your system will never handle paths like this before adopting this technique.Dewaynedewberry
@Giffard You can avoid those particular macros on a compiler-by-compiler basis, but you might not want such requirements in a header you distribute to users. I just encountered this technique used to handle an absolute path, failing in this manner due to the name of a parent directory.Dewaynedewberry
You can use -std=c++11 to avoid the linux and unix macros, but lots of projects define other macros that could appear in a path (perhaps in a parent directory).Dewaynedewberry
@jed: Ok, I added a warning. Thanks for the suggestion.Giffard
I can confirm that this works with Visual Studio 2019. (And I notice someone in another answer with the same solution for VS2013.)Keratose
@Giffard Is it possible to make this solution work in a way that STR macro can accept string literals, for example: #include STR("some/path/", "myfile.h")?Gaulish
@IvanR: No. There is no mechanism in the preprocessor to remove quotes from a token, and string literal concatenation happens after preprocessing, so it cannot be used in an #include directive. So you'll need to figure out how to do it without quotes. For file paths it's not usually a problem. In general, it is better to configure include paths using the facilities of your build environment, which should be better adapted to the task.Giffard
I believe the second instantiation of IDENT(), IDENT(y), in the replacement text of PATH() to be superfluous: the purpose of IDENT() is to delimit its argument from the next preprocessing token without introducing white space and y is the final token. # define PATH(x,y) STR(IDENT (x)y) Anyway thank you very much for this.Readymade
@user1254127: it is certainly unnecessary but it doesn't hurt anything, and I found it easier to read. But your way is just as good.Giffard
P
2

I would like to have include file paths dynamically created by a macro for a target-configuration-dependent part of my program.

You should be unable to (and if you are able to do so, you probably shouldn't do this).

You are effectively trying to do the compiler's job in a source file, which does not make much sense. If you want to change include paths based on the machine you compile on, this is a solved problem (but not solved in a header file).

Canonical solution:

Use an IF in your Makefile or CMakeLists.txt, use custom property pages depending on the build configuration in Visual Studio (or simply set the particular settings for your build in the OS environment for your user).

Then, write the include directive as:

#include <filename.h> // no path here

and rely on the environment/build system to make the path available when the compiler is invoked.

Pamalapamela answered 18/8, 2015 at 7:45 Comment(3)
are you able to qualify why this should not be possible? Adding a -I directive for source files in this part of the program is an option but it would (in my view) obfuscate the code because it would no longer be apparent that the required header was in a subdirectory that had been configured in CMakeLists.txt fileSnider
It should not be possible, because source code is not the place to configure include search paths for the build environment (if I had to write a compiler or compiler specification, I would not add macro token replacement in the include directive parameters - but I don't know if it's there). If you want to make it obvious that an included file belongs to a certain directory, add the include path all the way to the subdirectory in the build system and the subdirectory and file name in the source code.Pamalapamela
Such a notion seems effectively equivalent to throwing all the header files of a project into a single directory, except that naming conflicts won't result in files being overwritten but may instead result in the system choosing arbitrarily from among like-named files. Being able to define macros associated with path names so that every #include statement would definitively identify the header it needs seems MUCH cleaner.Screed
J
1

This works for VS2013. (It can be done easier, ofcourse.)

#define myIDENT(x) x
#define myXSTR(x) #x
#define mySTR(x) myXSTR(x)
#define myPATH(x,y) mySTR(myIDENT(x)myIDENT(y))

#define myLIBAEdir D:\\Georgy\\myprojects\\LibraryAE\\build\\native\\include\\ //here whitespace!
#define myFile libae.h

#include myPATH(myLIBAEdir,myFile)
Jaramillo answered 25/8, 2016 at 7:7 Comment(0)
M
0

From your description, it sound like you discovered that not every "" is a string. In particular, #include "corefoundation/header.h" looks like an ordinary string but it isn't. Grammatically, quoted text outside preprocessor directives are intended for the compiler, and compile to null terminated string literals. Quoted text in preprocessor directives is interpreted by the preprocessor in an implementation-defined way.

That said, the error in your example is because Boost pasted the second and third token : / and filename. The first, fourth and fifth token (directory, . and h) are left unchanged. This is not what you wanted, obviously.

It's a lot easier to rely on automatic string concatenation. "directory/" "filename" is the same string literal as "directory/filename" Note that there is no + between the two fragments.

Mancilla answered 18/8, 2015 at 8:29 Comment(2)
unfortunately string concatenation does not happen in the preprocessor so that's not an option in my specific case.Snider
@RichardHodges: Well, that's technically up to the preprocessor IIRC. But as aI said, the quoted text in a proprocessor directive is not a string and does not necessarily behave like one. If it's not a string, then don't expect string concatenation.Mancilla

© 2022 - 2024 — McMap. All rights reserved.