[英]__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.