编译时处理__FILE__宏的操作

25

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

这里是问题。是否有一种使用模板和静态元编程的方式,在编译时运行basename()或类似函数?由于__FILE__在编译时是常量且已知的,因此这可能使它更容易实现。你认为呢?能做到吗?


3
一些快速的实验表明,__FILE__ 展开为命令行中给定的文件名,可以是绝对路径或相对路径。区别可能在于 Makefile 中。__BASE_FILE__ 是 gcc 扩展,与之不同的是,它只返回最外层的文件名,而不是任何 #include 的内容。 - Keith Thompson
9个回答

23
在使用CMake来驱动构建过程的项目中,您可以使用类似于这样的宏来实现可移植版本,在任何编译器或平台上都可以正常工作。尽管我个人对必须使用除gcc以外的其他东西感到遗憾... :)

在使用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解决方案的人来说,这是一个非常棒的答案。 - abdus_salam
1
我建议使用set_property(APPEND)而不是手动使用get_property()list(APPEND) - mathstuf
2
不幸的是,这并不是__FILE__的理想替代品,因为它只报告编译单元源文件的文件名,而不是您可能放置实际代码的任何头文件(__LINE__与之相关)。 - Steven Lu
@StevenLu,你说得很好。这是一个不完美的解决方案。因此它的有用性取决于你如何使用它。当你在实现文件中使用它进行断言时,它会按预期工作。 - Colin D Bennett
我勉强使用了 strrchr 的折衷方案。我们已经被承诺有编译时字符串操作一段时间了。它在哪里?唉。 - Steven Lu
我很蠢。这个页面上有多个可用的constexpr实现。 - Steven Lu

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函数,因此最好使用带有stmt-expr版本的gcc。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中完美运行,但是由于语句表达式是一种非标准扩展,正如您所说,这种解决方案并不普遍。我认为有一种方法可以在不依赖语句表达式的情况下强制进行编译时评估,使用带有整数参数的模板。我发布了一个答案来描述它,其中使用了此答案中的basename_index() - TerraPass

10

目前没有办法在编译时进行完整的字符串处理(在模板中我们能处理的最大长度是奇怪的四字符字面量)。

为什么不直接将处理后的名称保存为静态的呢,例如:

namespace 
{
  const std::string& thisFile() 
  {
      static const std::string s(prepocessFileName(__FILE__));
      return s;
  }
}
这样一来,您每个文件只需要完成一次工作。当然,您也可以将其包装成宏等。

9

你可能想尝试使用__BASE_FILE__宏。这个页面描述了gcc支持的许多宏。


2
非常好的观点。如果在Solaris下导致编译错误,并且您需要同时支持两者,请添加ifdef来检查BASE_FILE是否存在,如果不存在则使用__FILE__。 - Prof. Falken
2
实际上这不是一个很好的观点。在Solaris上,__BASE_FILE__仍然包括文件的路径,而不仅仅是它的名称组件。 - ScaryAardvark
1
请参见 https://www.mail-archive.com/cfe-users@cs.uiuc.edu/msg00556.html:BASE_FILE 是您正在编译的主文件,而不是您当前所在的包含头文件。 - MvG

8

另一个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(常量表达式)会在编译时尽可能执行,否则会退化为语句在运行时执行。

这种算法是有些不同的,它先找到字符串的结尾,然后向后查找最后一个反斜杠。它可能比其他答案慢一些,但由于旨在在编译时执行,因此不应该成为问题。


1
好的想法,但它不能正常工作。应该用“&&”代替“||”来修复它。 - Maxim Kholyavkin
1
唯一一个能够在 Microsoft Visual Studio 编译器中正常工作的答案! - user
2
我们可以通过将函数的结果存储在一个 constexpr 变量中,如 constexpr const char* myExpression = pathlast(__FILE__); std::cout << myExpression << "\n"; 来强制和保证其在编译时被评估。出处:在编译时计算 C 字符串的长度。这真的是一个 constexpr 吗? - user
1
这就是你要找的机器人。 - Steven Lu

8
我喜欢@Chetan Reddy的回答,他建议在语句表达式中使用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)

我几乎照搬了先前提到的答案中的basename_index()函数,只是增加了一个检查Windows特有的反斜杠分隔符。


6

在使用CMake时,另一种可能的方法是添加自定义预处理器定义,直接使用make自动变量(代价是某些人认为丑陋的转义):

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 会过度转义

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

(虽然 make 的文档建议使用 $(notdir path $<),但在添加定义时不要有空格似乎更能让 CMake 满意。)

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

#ifndef __FILENAME__
#define __FILENAME__ __FILE__
#endif

1
使用一个工具链时,这个方法很好用,但另一个工具链将cmake_pch.hxx.gch作为所有文件的__FILENAME__传递。可能是因为它已经将此文件添加到了make源代码的开头。 我最终使用了add_definitions(-D__FILENAME__=\\"$\(subst .o,,$\(@F\)\)\\"),这对我的两种情况都有效。它获取目标名称,通常为source_name.cpp.o,并删除.o - PolyGlot
1
我在使用cmake 3.24时不得不使用两个$字符:add_definitions(-D__FILENAME__=\\"$$\(<F\)\\"),但它有效,谢谢! - par

3

对于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
@abarisone,希望这个回答能够澄清问题。如果你想让我对任何方面进行更详细的阐述,请告诉我。 - lance-ios

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接