编译时__FILE__宏操作处理
我在将一些东西从Solaris移植到Linux时遇到的一个问题是Solaris编译器在预处理期间将宏__FILE__
扩展为文件名(例如MyFile.cpp),而Linux上的gcc扩展到完整路径(例如/ home) /user/MyFile.cpp)。 使用basename()可以很容易地解决这个问题但是……如果你经常使用它,那么对basename()的所有调用都必须加起来,对吧?
这是问题所在。 有没有办法使用模板和静态元编程,在编译时运行basename()或类似的? 由于__FILE__
是常量且在编译时已知,因此可能更容易。 你怎么看? 可以吗?
目前无法在编译时进行完整的字符串处理(我们可以在模板中使用的最大值是奇怪的四字符文字)。
为什么不简单地静态保存处理过的名称,例如:
namespace { const std::string& thisFile() { static const std::string s(prepocessFileName(__FILE__)); return s; } }
这样你每个文件只做一次工作。 当然你也可以将它包装成一个宏等。
使用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 #include 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; }
在使用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)
您可能想尝试__BASE_FILE__
宏。 这个页面描述了gcc支持的很多宏。
另一个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
将在编译时执行,否则它将回退到语句的运行时执行。
该算法略有不同,因为它找到字符串的结尾然后向后工作以找到最后一个斜杠。 它可能比其他答案慢,但由于它打算在编译时执行,因此它应该不是问题。
使用CMake时另一种可能的方法是添加一个自定义预处理器定义,该定义直接使用make
的自动变量 (代价是一些可以说是丑陋的转义):
add_definitions(-D__FILENAME__=\\"$\(
或者,如果您使用的是CMake> = 2.6.0:
cmake_policy(PUSH) cmake_policy(SET CMP0005 OLD) # Temporarily disable new-style escaping. add_definitions(-D__FILENAME__=\\"$\(
(否则CMake 会过度逃避 。)
在这里,我们利用事实使用源文件名替换$(
-D__FILENAME__=\"MyFile.cpp\"
。
(虽然make
的文档建议使用$(notdir path $<)
,但在添加的定义中没有空格似乎可以让CMake更好。)
然后,您可以在源代码中使用__FILENAME__
,就像使用__FILE__
。 出于兼容性目的,您可能需要添加安全后备:
#ifndef __FILENAME__ #define __FILENAME__ __FILE__ #endif
我喜欢@Chetan Reddy的回答 ,它建议在语句表达式中使用static_assert()
强制编译时调用函数查找最后一个斜杠,从而避免运行时开销。
但是,语句表达式是非标准扩展,并且不受普遍支持。 例如,我无法在Visual Studio 2017(MSVC ++ 14.1,我相信)下编译该答案的代码。
相反,为什么不使用带有整数参数的模板,例如:
template struct require_at_compile_time { static constexpr const int value = Value; };
定义了这样的模板后,我们可以使用@Chetan Reddy的答案中的basename_index()
函数:
require_at_compile_time::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 struct require_at_compile_time { static constexpr const int32_t value = Value; }; #define JUST_FILENAME (__FILE__ + require_at_compile_time::value)
我从前面提到的答案中几乎逐字地窃取了basename_index()
,除了我添加了对特定于Windows的反斜杠分隔符的检查。
对于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__)
有:
#define __BASENAME__ [[[NSString stringWithCString:__FILE__ \ encoding:NSUTF8StringEncoding] \ lastPathComponent] \ cStringUsingEncoding:NSUTF8StringEncoding] #define MyLog(fmt, ...) MyLog((@"E %s [Line %d] " fmt), \ __BASENAME__, __LINE__, ##__VA_ARGS__)
它确实包含一些运行时元素,并且在这个意义上并不完全符合问题,但它可能适用于大多数情况。