简体   繁体   中英

__FILE__ macro manipulation handling at compile time

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 (eg MyFile.cpp) whereas gcc on Linux expandeds out to the full path (eg /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?

In projects using CMake to drive the build process, you can use a macro like this to implement a portable version that works on any compiler or platform. Though personally I pity the fool who must use something other than gcc... :)

# Helper function to add preprocesor definition of FILE_BASENAME
# to pass the filename without directory path for debugging use.
#
# Note that in header files this is not consistent with
# __FILE__ and __LINE__ since FILE_BASENAME will be the
# compilation unit source file name (.c/.cpp).
#
# Example:
#
#   define_file_basename_for_sources(my_target)
#
# Will add -DFILE_BASENAME="filename" for each source file depended on
# by my_target, where filename is the name of the file.
#
function(define_file_basename_for_sources targetname)
    get_target_property(source_files "${targetname}" SOURCES)
    foreach(sourcefile ${source_files})
        # Add the FILE_BASENAME=filename compile definition to the list.
        get_filename_component(basename "${sourcefile}" NAME)
        # Set the updated compile definitions on the source file.
        set_property(
            SOURCE "${sourcefile}" APPEND
            PROPERTY COMPILE_DEFINITIONS "FILE_BASENAME=\"${basename}\"")
    endforeach()
endfunction()

Then to use the macro, just call it with the name of the CMake target:

define_file_basename_for_sources(myapplication)

Using C++11, you have a couple of options. Let's first define:

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] == '/'
             ? basename_index (path, index + 1, index)
             : basename_index (path, index + 1, slash_index)
           )
         : (slash_index + 1)
     ;
}

If your compiler supports statement expressions, and you want to be sure that the basename computation is being done at compile-time, you can do this:

// stmt-expr version
#define STRINGIZE_DETAIL(x) #x
#define STRINGIZE(x) STRINGIZE_DETAIL(x)

#define __FILELINE__ ({ static const int32_t basename_idx = basename_index(__FILE__);\
                        static_assert (basename_idx >= 0, "compile-time basename");  \
                        __FILE__ ":" STRINGIZE(__LINE__) ": " + basename_idx;})

If your compiler doesn't support statement expressions, you can use this version:

// non stmt-expr version
#define __FILELINE__ (__FILE__ ":" STRINGIZE(__LINE__) ": " + basename_index(__FILE__))

With this non stmt-expr version, gcc 4.7 and 4.8 call basename_index at run-time, so you're better off using the stmt-expr version with gcc. ICC 14 produces optimal code for both versions. ICC13 can't compile the stmt-expr version, and produces suboptimal code for the non stmt-expr version.

Just for completeness, here's the code all in one place:

#include <iostream>
#include <stdint.h>

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] == '/'
           ? basename_index (path, index + 1, index)
           : basename_index (path, index + 1, slash_index)
           )
       : (slash_index + 1)
       ;
}

#define STRINGIZE_DETAIL(x) #x
#define STRINGIZE(x) STRINGIZE_DETAIL(x)

#define __FILELINE__ ({ static const int32_t basename_idx = basename_index(__FILE__); \
                        static_assert (basename_idx >= 0, "compile-time basename");   \
                        __FILE__ ":" STRINGIZE(__LINE__) ": " + basename_idx;})


int main() {
  std::cout << __FILELINE__ << "It works" << std::endl;
}

There is currently no way of doing full string processing at compile time (the maximum we can work with in templates are the weird four-character-literals).

Why not simply save the processed name statically, eg:

namespace 
{
  const std::string& thisFile() 
  {
      static const std::string s(prepocessFileName(__FILE__));
      return s;
  }
}

This way you are only doing the work once per file. Of course you can also wrap this into a macro etc.

you might want to try the __BASE_FILE__ macro. This page describes a lot of macros which gcc supports.

Another C++11 constexpr method is as follows:

constexpr const char * const strend(const char * const str) {
    return *str ? strend(str + 1) : str;
}

constexpr const char * const fromlastslash(const char * const start, const char * const end) {
    return (end >= start && *end != '/' && *end != '\\') ? fromlastslash(start, end - 1) : (end + 1);
}

constexpr const char * const pathlast(const char * const path) {
    return fromlastslash(path, strend(path));
}

The usage is pretty simple also:

std::cout << pathlast(__FILE__) << "\n";

The constexpr will be performed at compile-time if possible, otherwise it will fallback to run-time execution of the statements.

The algorithm is a little different in that it finds the end of the string and then works backwards to find the last slash. It is probably slower than the other answer but since it is intended to be executed at compile-time it shouldn't be an issue.

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

For Objective-C the following macro provides a CString which can replace the __FILE__ macro, but omitting the initial path components.

#define __BASENAME__ [[[NSString stringWithCString:__FILE__              \
                                        encoding:NSUTF8StringEncoding]   \
                                                    lastPathComponent]   \
                            cStringUsingEncoding:NSUTF8StringEncoding]   

That is to say it converts: /path/to/source/sourcefile.m into: sourcefile.m

It works by taking the ouptput of the __FILE__ macro (which is a C-formatted, null terminated string), converting it to an Objective-C string object, then stripping out the initial path components and finally converting it back into a C formatted string.

This is useful to get logging format which is more readable, replacing, (for example) a logging macro like this:

#define MyLog(fmt, ...) MyLog((@"E %s [Line %d] " fmt),                \
                               __FILE__, __LINE__, ##__VA_ARGS__)

with:

#define __BASENAME__ [[[NSString stringWithCString:__FILE__            \
                                        encoding:NSUTF8StringEncoding] \
                                                    lastPathComponent] \
                            cStringUsingEncoding:NSUTF8StringEncoding]

#define MyLog(fmt, ...) MyLog((@"E %s [Line %d] " fmt),                \
                               __BASENAME__, __LINE__, ##__VA_ARGS__)

It does contain some runtime elements, and in that sense does not fully comply with the question, but it is probably appropriate for most circumstances.

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);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM