1

I am trying include a file constructed from pre-processor macros, but running into a wall due to rules regarding tokens, it seems. I used the answer here as a reference: Concatenate string in C #include filename, but my case differs in that there are decimal points in the define I am using to construct my include. This is what I have currently that will not get through the preprocessor stage: main.c:

#include <stdio.h>
#include <stdlib.h>

#define VERSION 1.1.0
#define STRINGIFY(arg) #arg
#define INCLUDE_HELPER(arg) STRINGIFY(other_ ##arg.h)
#define INCLUDE_THIS(arg) INCLUDE_HELPER(arg)

#include INCLUDE_THIS(VERSION)

int main(int argc, char **argv) {

   printf(INCLUDE_THIS(VERSION));
   fflush(stdout);
#if defined (SUCCESS)
   printf("\nSUCCESS!\n");
#endif
   return EXIT_SUCCESS;
}

other_1.1.0.h:

#define SUCCESS

Were I to use #define VERSION 1_1_0 and renamed the header accordingly it would work (but not viable for my use as I have no control over the name of the header files the actual project uses), but 1.1.0 is not a valid preprocessor token.

EDIT: After a bit more digging through the documentation, I see that 1.1.0 is a valid preprocessing number; it is the resulting concatenation of other_1.1.0 that is invalid. Regardless, the issue of not being able to construct the include remains.

Christian Gibbons
  • 4,005
  • 1
  • 12
  • 27
  • Have you considered alternative ways of providing version-specific information so that you don't run into this conundrum? Or is that not possible? – Jonathan Leffler Mar 26 '18 at 19:43
  • @JonathanLeffler It is a hard requirement that a different header be included based on the version. There should be some flexibility in how to include the header. I am open to other options if it results in an elegant solution. – Christian Gibbons Mar 26 '18 at 19:55
  • Ok; go back to those who set the requirement and ask them how to do it. If they don't know, maybe they shouldn't be setting the requirements? The developers are producing the version-specific headers (and won't change their scheme); ask them how they include the correct version in their test code, etc. Have you considered using symbolic links, for example, so you `#include "other_version.h"` but `other_version.h` is a symbolic link to `other_1.1.0.h` this week, and `other_1.1.1.h` next week, etc. – Jonathan Leffler Mar 26 '18 at 19:56
  • @JonathanLeffler That would be a bit above my pay grade. And this isn't so much an insurmountable problem as there are several possible solutions to include the right header, I was just attempting a solution that wouldn't require a series of `#ifdef`s for each version or passing in the entire file name to be included as a define. – Christian Gibbons Mar 26 '18 at 20:10
  • I can't answer for the politics in your organization, but you should be able to ask for advice from those who should know how to provide it. There are problems if those who know won't share that knowledge with those who ask politely for it. They may have a dramatically different solution — they may have something that already deals with the problem. Maybe the `other_version.h` header is what contains the gruesome `#ifdef`ery to select/include the correct version header. You certainly don't want the paragraph of macro-hackery that you show replicated — it should be in one file only. – Jonathan Leffler Mar 26 '18 at 20:14
  • And how is `VERSION` going to be set in practice? Will it be a command-line `-DVERSION=1.1.0` macro, or will it be literally in the source code as you show? (I'm mostly trying to help you think out of the box — since the box you're in won't let you do what you want/need cleanly.) – Jonathan Leffler Mar 26 '18 at 20:15
  • @JonathanLeffler The politics/communication lines are complicated due to it involving multiple companies. As for not repeating the hackery, I've already taken care of that. In the actual project (rather than this MCVE) I have a one header that only contains the "magic" to include the actual header that is desired. And for setting `VERSION`, that was my plan; pass it in through the compiler. – Christian Gibbons Mar 26 '18 at 20:18
  • 1
    If you can do `-DVERSION=${version}` in a shell script/makefile, you could consider doing `-DVERSION_HEADER='"other_${version}.h"'` where the single quotes prevent the shell from stripping off the double quotes — though you can also arrange to stringify VERSION_HEADER inside your code. It's a bit of a nuisance, but fixes the problems with the preprocessor by not using the preprocessor to fix them. In the code, you write `#include VERSION_HEADER` under this scheme (or maybe `#include STRINGIFY(VERSION_HEADER)`. – Jonathan Leffler Mar 26 '18 at 20:21
  • @JonathanLeffler Hmm, that could work. – Christian Gibbons Mar 26 '18 at 20:27
  • 1
    You might need to do the 'double macro dance' to get things stringified correctly. See [SO 195975](https://stackoverflow.com/questions/195975/) for the `#` (stringification) variant, and [SO 1489932](https://stackoverflow.com/questions/1489932/) for the `##` (token pasting) variant. – Jonathan Leffler Mar 26 '18 at 20:27
  • Made my comment into an answer, since it wasn't going to be seen. Although it's really a dup. – rici Mar 27 '18 at 05:01

3 Answers3

2

With some experimentation, I came up with a solution that, while not ideal, could be workable.

#define VERSION _1.1.0
#define STRINGIFY(arg) #arg
#define INCLUDE_HELPER(arg) STRINGIFY(other ##arg.h)
#define INCLUDE_THIS(arg) INCLUDE_HELPER(arg)

#include INCLUDE_THIS(VERSION)

Rather than pasting other_ and 1.1.0 together, I am pasting other and _1.1.0. I am not sure why this is acceptable as the resulting token is the same, but there it is.

I would still prefer to have a solution that allows me to just define the version number without the underscore, so I will hold off on accepting this answer in case someone can come up with a more elegant solution (and works for people who don't happen to need an underscore anyways)

Christian Gibbons
  • 4,005
  • 1
  • 12
  • 27
  • 1
    Token pasting combines two consecutive tokens. `other_` is an identifier token `1.1.1` is a preprocessing number. The concatenation of those two strings is not a token. But the other way is quite different: `other` is an identifier token. So is `_1`. `.1.1` is a ppnumber token, but it is not an argument of the token pasting operator. The operands are `other` and `_1`, which concatenate into the identifier token `other_1`. Stringification is then applied to the token sequence `other_1` `.1.1`. Moral: stringification has nothing to do with tokenising. – rici Mar 28 '18 at 04:49
2

It's easy once you stop thinking about token concatenation. Stringification works with any sequence of tokens, so there is no need to force its argument into being a single token. You do need an extra indirection so that the argument is expanded, but that's normal.

The only trick is to write the sequence without whitespace, which is what ID is for:

#define STRINGIFY(arg) STRINGIFY_(arg)
#define STRINGIFY_(arg) #arg
#define ID(x) x

#define VERSION 1.1.0
#include STRINGIFY(ID(other_)VERSION.h)

See https://stackoverflow.com/a/32077478/1566221 for a longer explanation.

rici
  • 201,785
  • 23
  • 193
  • 283
  • Ah, that makes sense. The ID macro allows you to to place the `VERSION` token right up next to the prefix without use of the token-pasting operator bypassing the rules of token pasting. – Christian Gibbons Mar 27 '18 at 13:57
1

If you are passing -DVERSION=1.1.0 as a compile-line parameter, rather than hard-wiring it in the source code, then there's nothing to stop you passing a second define using make or the shell to do the concatenation. For example, in a makefile, you might have:

VERSION = 1.1.0
VERSION_HEADER = other_${VERSION}.h

CFLAGS += -DVERSION=${VERSION} -DVERSION_HEADER=${VERSION_HEADER}

and then:

#include <stdio.h>
#include <stdlib.h>

#define STRINGIFY(arg) #arg
#define INCLUDE_HELPER(arg) STRINGIFY(arg)
#define INCLUDE_THIS(arg) INCLUDE_HELPER(arg)

#include INCLUDE_THIS(VERSION_HEADER)

int main(void)
{
   printf("%s\n", INCLUDE_THIS(VERSION));
#if defined (SUCCESS)
   printf("SUCCESS!\n");
#endif
   return EXIT_SUCCESS;
}

which is basically your code with the #define VERSION line removed, and using the stringified version of VERSION_HEADER instead of trying to construct the header name in the source code. You might want to use:

#ifndef VERSION
#define VERSION 1.1.0
#endif
#ifndef VERSION_HEADER
#define VERSION_HEADER other_1.1.0.h
#endif

for some suitable default fallback version in case the person running the compilation doesn't specify the information on the command line. Or you might use #error You did not set -DVERSION=x.y.z on the command line instead of setting the default value.

When compiled (source file hdr59.c):

$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror -DVERSION=1.1.0 \
>     -DVERSION_HEADER=other_1.1.0.h hdr59.c -o hdr59
$ ./hdr59
1.1.0
SUCCESS!
$

I would put the three lines of macro and the #include line into a separate small header so that it can be included when the version header is needed. If the default setting is required too, then that adds to the importance of putting the code into a separate header for reuse. The program's source code might contain:

#include "other_version.h"

and that header would arrange to include the correct file, more or less as shown.

Jonathan Leffler
  • 666,971
  • 126
  • 813
  • 1,185