【问题标题】:__FILE__ macro manipulation handling at compile time__FILE__ 编译时的宏操作处理
【发布时间】:2010-12-14 22:46:12
【问题描述】:

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

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

【问题讨论】:

  • 一些快速实验表明__FILE__ 扩展为命令行中给出的文件名,可以是绝对的或相对的。差异可能在 Makefile 中。 __BASE_FILE__,一个 gcc 扩展,不同之处仅在于它产生最外层的文件名,而不是任何 #included。

标签: c++ c templates metaprogramming


【解决方案1】:

在使用 CMake 驱动构建过程的项目中,您可以使用这样的宏来实现可在任何编译器或平台上运行的可移植版本。虽然我个人同情必须使用 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()

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

define_file_basename_for_sources(myapplication)

【讨论】:

  • 该操作没有提到 cmake,但对于追求 cmake 解决方案的人来说,这是一个了不起的答案。
  • 我建议编辑使用set_property(APPEND) 而不是手动使用get_property()list(APPEND)
  • 不幸的是,它不能很好地替代__FILE__,因为它只报告编译单元源文件的文件名,而不是您可能将实际代码放入的任何标题(LINE 与)有关。
  • @StevenLu 这是一个很好的观点。这是一个不完美的解决方案。所以它的用处取决于你如何使用它。当您将它用于实现文件中的断言时,它会按预期工作。
  • 我不情愿地选择了strrchr kludge。一段时间以来,我们一直承诺进行编译时字符串操作。它在哪里?啊。
【解决方案2】:

使用 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;
}

【讨论】:

  • 在语句表达式中使用 static_assert() 的想法是一个非常好的想法,并且可以与 GCC 完美配合,但是,由于语句表达式是非标准扩展,正如您所说,这种解决方案并不通用.我认为有一种方法可以强制编译时评估,而不依赖于语句表达式,使用带有整数参数的模板。我发布了一个answer 来描述它,它使用了这个答案中的basename_index()
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 好点。如果这导致 Solaris 下的编译错误并且您必须同时支持两者,请添加 ifdefs 以检查 BASE_FILE 并在 BASE_FILE 不存在时使用 FILE
    • 其实这不是一个很好的点。 solaris 上的 BASE_FILE 仍然包括文件的路径,而不仅仅是它的名称组件。
    • 参见mail-archive.com/cfe-users@cs.uiuc.edu/msg00556.htmlBASE_FILE 是您正在编译的主文件,而不是您当前所在的包含头文件。
    【解决方案5】:

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

    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 将在编译时执行,否则将回退到语句的运行时执行。

    该算法有点不同,它先找到字符串的结尾,然后向后查找最后一个斜杠。它可能比其他答案慢,但由于它打算在编译时执行,所以应该不是问题。

    【讨论】:

    • 好主意,但它不能正常工作。 ||应该替换为 && 来修复它。
    • 唯一真正适用于 Microsoft Visual Studio 编译器的答案!
    • 我们可以 forcegarante 通过将函数的结果存储在 constexpr 变量为constexpr const char* myExpression = pathlast(__FILE__); std::cout &lt;&lt; myExpression &lt;&lt; "\n";。来源:Computing length of a C string at compile time. Is this really a constexpr?
    • 这就是你要找的机器人
    【解决方案6】:

    我喜欢@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】:

      使用 CMake 时的另一种可能方法是添加直接使用 makeautomatic variables 的自定义预处理器定义(代价是一些可以说是丑陋的转义):

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

      或者,如果您使用的是 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)
      

      (否则 CMake will over-escape things.)

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

      (虽然make 的文档建议改用$(notdir path $&lt;),但在添加的定义中没有空格似乎更能取悦CMake。)

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

      #ifndef __FILENAME__
      #define __FILENAME__ __FILE__
      #endif
      

      【讨论】:

      • 使用一个工具链运行良好,但另一个工具链将cmake_pch.hxx.gch 作为__FILENAME__ 传递给所有文件。可能是因为它已将此文件添加到 make 的源代码的开头。我最终使用了适用于我的两种情况的add_definitions(-D__FILENAME__=\\"$\(subst .o,,$\(@F\)\)\\")。它采用目标名称,通常为source_name.cpp.o,并删除.o
      【解决方案8】:

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

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

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

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

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

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

      与:

      #define __BASENAME__ [[[NSString stringWithCString:__FILE__            \
                                              encoding:NSUTF8StringEncoding] \
                                                          lastPathComponent] \
                                  cStringUsingEncoding:NSUTF8StringEncoding]
      
      #define MyLog(fmt, ...) MyLog((@"E %s [Line %d] " fmt),                \
                                     __BASENAME__, __LINE__, ##__VA_ARGS__)
      

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

      【讨论】:

      • 能否请您详细说明您的答案,添加更多关于您提供的解决方案的描述?
      • @abarisone,希望能澄清答案。如果您希望我详细说明任何方面,请告诉我。
      【解决方案9】:

      我已将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);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-04-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多