__FILE__ macro manipulation handling at compile time

Viewed 13043

One of the issues I have had in porting some stuff from Solaris to Linux is that the Solaris compiler expands the macro __FILE__ during preprocessing to the file name (e.g. MyFile.cpp) whereas gcc on Linux expandeds out to the full path (e.g. /home/user/MyFile.cpp). This can be reasonably easily resolved using basename() but....if you're using it a lot, then all those calls to basename() have got to add up, right?

Here's the question. Is there a way using templates and static metaprogramming, to run basename() or similar at compile time? Since __FILE__ is constant and known at compile time this might make it easier. What do you think? Can it be done?

9 Answers

I like @Chetan Reddy's answer, which suggests using static_assert() in a statement expression to force a compile time call to function finding the last slash, thus avoiding runtime overhead.

However, statement expressions are a non-standard extension and are not universally supported. For instance, I was unable to compile the code from that answer under Visual Studio 2017 (MSVC++ 14.1, I believe).

Instead, why not use a template with integer parameter, such as:

template <int Value>
struct require_at_compile_time
{
    static constexpr const int value = Value;
};

Having defined such a template, we can use it with basename_index() function from @Chetan Reddy's answer:

require_at_compile_time<basename_index(__FILE__)>::value

This ensures that basename_index(__FILE__) will in fact be called at compile time, since that's when the template argument must be known.

With this, the complete code for, let's call it JUST_FILENAME, macro, evaluating to just the filename component of __FILE__ would look like this:

constexpr int32_t basename_index (
    const char * const path, const int32_t index = 0, const int32_t slash_index = -1
)
{
     return path [index]
         ? ((path[index] == '/' || path[index] == '\\')  // (see below)
             ? basename_index (path, index + 1, index)
             : basename_index (path, index + 1, slash_index)
           )
         : (slash_index + 1)
     ;
}

template <int32_t Value>
struct require_at_compile_time
{
    static constexpr const int32_t value = Value;
};

#define JUST_FILENAME (__FILE__ + require_at_compile_time<basename_index(__FILE__)>::value)

I've stolen basename_index() almost verbatim from the previously mentioned answer, except I added a check for Windows-specific backslash separator.

Another possible approach when using CMake is to add a custom preprocessor definition that directly uses make's automatic variables (at the cost of some arguably ugly escaping):

add_definitions(-D__FILENAME__=\\"$\(<F\)\\")

Or, if you're using CMake >= 2.6.0:

cmake_policy(PUSH)
cmake_policy(SET CMP0005 OLD) # Temporarily disable new-style escaping.
add_definitions(-D__FILENAME__=\\"$\(<F\)\\")
cmake_policy(POP)

(Otherwise CMake will over-escape things.)

Here, we take advantage of the fact make substitutes $(<F) with the source file name without leading components and this should show up as -D__FILENAME__=\"MyFile.cpp\" in the executed compiler command.

(While make's documentation recommends using $(notdir path $<) instead, not having whitespace in the added definition seems to please CMake better.)

You can then use __FILENAME__ in your source code like you'd use __FILE__. For compatibility purposes you may want to add a safe fallback:

#ifndef __FILENAME__
#define __FILENAME__ __FILE__
#endif

I've compressed the constexpr version down to one recursive function that finds the last slash and returns a pointer to the character after the slash. Compile time fun.

constexpr const char* const fileFromPath(const char* const str, const char* const lastslash = nullptr) {
    return *str ? fileFromPath(str + 1, ((*str == '/' || *str == '\\') ? str + 1 : (nullptr==lastslash?str:lastslash)) : (nullptr==lastslash?str:lastslash);
}
Related