编译时__FILE__宏操作处理

时间:2009-11-10 08:19:46

标签: c++ c templates metaprogramming

我将一些东西从Solaris迁移到Linux时遇到的一个问题是Solaris编译器在预处理期间将宏__FILE__扩展为文件名(例如MyFile.cpp),而Linux上的gcc扩展为完整路径(例如/home/user/MyFile.cpp)。使用basename()可以很容易地解决这个问题但是......如果你经常使用它,那么对basename()的所有调用都必须加起来,对吧?

这是问题所在。有没有办法使用模板和静态元编程,在编译时运行basename()或类似的?由于__FILE__是常量且在编译时已知,因此可能会更容易。你怎么看?可以吗?

9 个答案:

答案 0 :(得分:20)

使用C ++ 11,您有几个选择。我们先来定义:

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

如果您的编译器支持语句表达式,并且您希望确保在编译时完成基本名称计算,则可以执行以下操作:

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

如果编译器不支持语句表达式,则可以使用此版本:

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

使用这个非stmt-expr版本,gcc 4.7和4.8在运行时调用basename_index,因此最好使用带有gcc的stmt-expr版本。 ICC 14为这两个版本生成最佳代码。 ICC13无法编译stmt-expr版本,并为非stmt-expr版本生成次优代码。

为了完整起见,这里的代码全部在一个地方:

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

答案 1 :(得分:13)

在使用CMake驱动构建过程的项目中,您可以使用这样的宏来实现适用于任何编译器或平台的可移植版本。虽然我个人但是如果你必须使用除了gcc之外的其他东西我可怜你...... :)

# Helper function to add preprocesor definition of FILE_BASENAME
# to pass the filename without directory path for debugging use.
#
# 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})
        # Get source file's current list of compile definitions.
        get_property(defs SOURCE "${sourcefile}"
            PROPERTY COMPILE_DEFINITIONS)
        # Add the FILE_BASENAME=filename compile definition to the list.
        get_filename_component(basename "${sourcefile}" NAME)
        list(APPEND defs "FILE_BASENAME=\"${basename}\"")
        # Set the updated compile definitions on the source file.
        set_property(
            SOURCE "${sourcefile}"
            PROPERTY COMPILE_DEFINITIONS ${defs})
    endforeach()
endfunction()

然后要使用宏,只需使用CMake目标的名称调用它:

define_file_basename_for_sources(myapplication)

答案 2 :(得分:9)

目前无法在编译时进行完整的字符串处理(我们可以在模板中使用的最大值是奇怪的四字符文字)。

为什么不简单地静态保存处理过的名称,例如:

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

这样你每个文件只做一次工作。当然你也可以将它包装成一个宏等。

答案 3 :(得分:8)

你可能想尝试__BASE_FILE__宏。这个page描述了gcc支持的很多宏。

答案 4 :(得分:5)

另一种C ++ 11 constexpr方法如下:

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

用法非常简单:

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

如果可能的话,constexpr将在编译时执行,否则它将回退到语句的运行时执行。

算法略有不同,因为它找到了字符串的结尾,然后向后工作以找到最后一个斜杠。它可能比其他答案慢,但因为它打算在编译时执行它不应该是一个问题。

答案 5 :(得分:4)

使用CMake时另一种可能的方法是添加一个直接使用make的{​​{3}}的自定义预处理器定义(代价是一些可以说是丑陋的逃避):

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

或者,如果您使用的是CMake&gt; = 2.6.0:

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

(否则CMake automatic variables。)

在这里,我们利用事实make$(<F)替换为源文件名而不使用前导组件,这应该在执行的编译器命令中显示为-D__FILENAME__=\"MyFile.cpp\"

(虽然make的文档建议使用$(notdir path $<),但添加的定义中没有空格似乎可以让CM更好。)

然后,您可以在源代码中使用__FILENAME__,就像使用__FILE__一样。出于兼容性目的,您可能需要添加安全后备:

#ifndef __FILENAME__
#define __FILENAME__ __FILE__
#endif

答案 6 :(得分:4)

我喜欢@Chetan Reddy's answer,它建议在语句表达式中使用static_assert()强制编译时调用函数查找最后一个斜杠,从而避免运行时开销。

但是,语句表达式是非标准扩展,并且不受普遍支持。例如,我无法在Visual Studio 2017下编译该答案的代码(MSVC ++ 14.1,我相信)。

相反,为什么不使用带有整数参数的模板,例如:

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

定义了这样的模板之后,我们可以使用来自@Chetan Reddy的答案的basename_index()函数:

require_at_compile_time<basename_index(__FILE__)>::value

这确保basename_index(__FILE__)实际上在编译时被调用,因为那时必须知道模板参数。

这样,完整的代码,我们称之为JUST_FILENAME,宏,只评估__FILE__的文件名组件,如下所示:

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)

我几乎逐字地从previously mentioned answer偷了basename_index(),除了我添加了对特定于Windows的反斜杠分隔符的检查。

答案 7 :(得分:2)

对于Objective-C,下面的宏提供了一个CString,可以替换__FILE__宏,但省略了初始路径组件。

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

也就是说它将/path/to/source/sourcefile.m转换为:sourcefile.m

它的工作原理是获取__FILE__宏的ouptput(这是一个C格式的,以空字符结尾的字符串),将其转换为Objective-C字符串对象,然后剥离出初始路径组件并最终转换它回到C格式的字符串。

这对于获取更具可读性的日志记录格式非常有用,可以替换(例如)像这样的日志记录宏:

#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__)

它确实包含一些运行时元素,从这个意义上讲并不完全符合这个问题,但它可能适用于大多数情况。

答案 8 :(得分:0)

我已将 constexpr 版本压缩为一个递归函数,该函数查找最后一个斜杠并返回指向斜杠后字符的指针。编译时间很有趣。

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