[英]__FILE__ macro manipulation handling at compile time
我在将一些东西从 Solaris 移植到 Linux 时遇到的问题之一是 Solaris 编译器在预处理期间将宏__FILE__
扩展为文件名(例如 MyFile.cpp),而 Linux 上的 gcc 扩展到完整路径(例如 /home /user/MyFile.cpp)。 使用 basename() 可以很容易地解决这个问题,但是....如果你经常使用它,那么所有对 basename() 的调用都必须加起来,对吧?
这是问题。 有没有办法使用模板和静态元编程,在编译时运行 basename() 或类似的? 由于__FILE__
是常量并且在编译时已知,这可能会使它更容易。 你怎么认为? 可以做到吗?
在使用 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)
使用 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)
;
}
如果您的编译器支持语句表达式,并且您想确保在编译时完成 basename 计算,您可以这样做:
// 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;
}
目前没有办法在编译时进行完整的字符串处理(我们可以在模板中使用的最大值是奇怪的四字符文字)。
为什么不简单地静态保存处理过的名称,例如:
namespace
{
const std::string& thisFile()
{
static const std::string s(prepocessFileName(__FILE__));
return s;
}
}
这样你每个文件只做一次工作。 当然你也可以把它包装成一个宏等。
您可能想尝试__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
将在编译时执行,否则它将回退到语句的运行时执行。
该算法略有不同,因为它找到字符串的结尾,然后向后工作以找到最后一个斜杠。 它可能比其他答案慢,但由于它打算在编译时执行,因此不应该成为问题。
我喜欢@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 特定反斜杠分隔符的检查。
使用 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\\"
,这应该在执行的编译器命令中显示为-D__FILENAME__=\\"MyFile.cpp\\"
。
(虽然make
的文档建议使用$(notdir path $<)
代替,但在添加的定义中没有空格似乎更能取悦 CMake。)
然后,您可以在源代码中使用__FILENAME__
,就像使用__FILE__
。 出于兼容性目的,您可能需要添加安全回退:
#ifndef __FILENAME__
#define __FILENAME__ __FILE__
#endif
对于 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__)
它确实包含一些运行时元素,从这个意义上说并不完全符合问题,但它可能适用于大多数情况。
我已将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);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.