我有非常大的代码库,它广泛使用
__FILE__
进行日志记录。但是,它包含完整路径,这是 (1) 不需要的,(2) 可能会发生安全违规。
我正在尝试编写编译时子字符串表达式。最终得到了这个解决方案
static constexpr cstr PastLastSlash(cstr str, cstr last_slash)
{
return *str == '\0' ? last_slash : *str == '/' ? PastLastSlash(str + 1, str + 1) : PastLastSlash(str + 1, last_slash);
}
static constexpr cstr PastLastSlash(cstr str)
{
return PastLastSlash(str, str);
}
// usage
PastLastSlash(__FILE__);
这很好用,我检查了汇编代码,在编译时修剪了行,只有文件名存在于二进制中。
但是,这种表示法太冗长了。我想为此使用宏,但失败了。上面链接中建议的示例
#define __SHORT_FILE__ ({constexpr cstr sf__ {past_last_slash(__FILE__)}; sf__;})
不适用于 MSVC 编译器(我使用的是 MSVC 2017)。使用c++17还有其他方法吗?
UPD1: clang 由函数修剪 https://godbolt.org/z/tAU4j7
UPD2:看起来可以使用函数在编译时进行修剪,但完整的字符串将以二进制形式存在。
这个想法是创建截断的字符数组,但它只需要使用编译时功能。通过带有字符包的可变参数模板生成数据数组会强制编译器生成与传递的字符串文字没有直接关系的数据。这样编译器就无法使用输入字符串文字,尤其是当该字符串很长时。
Godbolt 与 clang:https://godbolt.org/z/WdKNjB.
Godbolt 与 msvc:https://godbolt.org/z/auMEIH.
唯一的问题是模板深度编译器设置。
首先我们定义 int 变量模板来存储索引序列:
template <int... I>
struct Seq {};
将 int 推入
Seq
:
template <int V, typename T>
struct Push;
template <int V, int... I>
struct Push<V, Seq<I...>>
{
using type = Seq<V, I...>;
};
创建序列:
template <int From, int To>
struct MakeSeqImpl;
template <int To>
struct MakeSeqImpl<To, To>
{
using type = Seq<To>;
};
template <int From, int To>
using MakeSeq = typename MakeSeqImpl<From, To>::type;
template <int From, int To>
struct MakeSeqImpl : Push<From, MakeSeq<From + 1, To>> {};
现在我们可以制作编译时整数序列,这意味着
MakeSeq<3,7> == Seq<3,4,5,6,7>
。我们仍然需要一些东西来在数组中存储选定的字符,但是使用编译时表示,这是带有字符的可变模板参数:
template<char... CHARS>
struct Chars {
static constexpr const char value[] = {CHARS...};
};
template<char... CHARS>
constexpr const char Chars<CHARS...>::value[];
接下来我们将选定的字符提取为
Chars
类型:
template<typename WRAPPER, typename IDXS>
struct LiteralToVariadicCharsImpl;
template<typename WRAPPER, int... IDXS>
struct LiteralToVariadicCharsImpl<WRAPPER, Seq<IDXS...> > {
using type = Chars<WRAPPER::get()[IDXS]...>;
};
template<typename WRAPPER, typename SEQ>
struct LiteralToVariadicChars {
using type = typename LiteralToVariadicCharsImpl<WRAPPER, SEQ> :: type;
};
WRAPPER
是包含字符串文字的类型。
快完成了。缺少的部分是找到最后一个斜杠。我们可以使用问题中找到的代码的修改版本,但这次它返回偏移量而不是指针:
static constexpr int PastLastOffset(int last_offset, int cur, const char * const str)
{
if (*str == '\0') return last_offset;
if (*str == '/') return PastLastOffset(cur + 1, cur + 1, str + 1);
return PastLastOffset(last_offset, cur + 1, str + 1);
}
获取字符串大小的最后一个实用程序:
constexpr int StrLen(const char * str) {
if (*str == '\0') return 0;
return StrLen(str + 1) + 1;
}
使用定义将所有内容组合在一起:
#define COMPILE_TIME_PAST_LAST_SLASH(STR) \
[](){ \
struct Wrapper { \
constexpr static const char * get() { return STR; } \
}; \
using Seq = MakeSeq<PastLastOffset(0, 0, Wrapper::get()), StrLen(Wrapper::get())>; \
return LiteralToVariadicChars<Wrapper, Seq>::type::value; \
}()
Lambda 函数是为了在使用这个宏时有很好的、类似于值的感觉。它还创建了定义
Wrapper
结构的范围。使用宏生成插入字符串文字的结构,会导致字符串文字绑定到类型的情况。
老实说我不会在生产中使用这种代码。它正在杀死编译器。
出于安全原因和内存使用的考虑,我建议使用带有自定义短路径的 docker 进行构建。
std::string_view
:
constexpr auto filename(std::string_view path)
{
return path.substr(path.find_last_of('/') + 1);
}
用途:
static_assert(filename("/home/user/src/project/src/file.cpp") == "file.cpp");
static_assert(filename("./file.cpp") == "file.cpp");
static_assert(filename("file.cpp") == "file.cpp");
查看它编译(godbolt.org)。
对于 Windows:
constexpr auto filename(std::wstring_view path)
{
return path.substr(path.find_last_of(L'\\') + 1);
}
使用 C++17,您可以执行以下操作(https://godbolt.org/z/68PKcsPzs):
#include <cstdio>
#include <array>
namespace details {
template <const char *S, size_t Start = 0, char... C>
struct PastLastSlash {
constexpr auto operator()() {
if constexpr (S[Start] == '\0') {
return std::array{C..., '\0'};
} else if constexpr (S[Start] == '/') {
return PastLastSlash<S, Start + 1>()();
} else {
return PastLastSlash<S, Start + 1, C..., (S)[Start]>()();
}
}
};
}
template <const char *S>
struct PastLastSlash {
static constexpr auto a = details::PastLastSlash<S>()();
static constexpr const char * value{a.data()};
};
int main() {
static constexpr char f[] = __FILE__;
puts(PastLastSlash<f>::value);
return 0;
}
对于 C++14,由于 constexpr 更有限,所以情况有点复杂(https://godbolt.org/z/bzGec5GMv):
#include <cstdio>
#include <array>
namespace details {
// Generic form: just add the character to the list
template <const char *S, char ch, size_t Start, char... C>
struct PastLastSlash {
constexpr auto operator()() {
return PastLastSlash<S, S[Start], Start + 1, C..., ch>()();
}
};
// Found a '/', reset the character list
template <const char *S, size_t Start, char... C>
struct PastLastSlash<S, '/', Start, C...> {
constexpr auto operator()() {
return PastLastSlash<S, S[Start], Start + 1>()();
}
};
// Found the null-terminator, ends the search
template <const char *S, size_t Start, char... C>
struct PastLastSlash<S, '\0', Start, C...> {
constexpr auto operator()() {
return std::array<char, sizeof...(C)+1>{C..., '\0'};
}
};
}
template <const char *S>
struct PastLastSlash {
const char * operator()() {
static auto a = details::PastLastSlash<S, S[0], 0>()();
return a.data();
}
};
static constexpr char f[] = __FILE__;
int main() {
puts(PastLastSlash<f>{}());
return 0;
}
使用 C++20 (https://godbolt.org/z/PKEhxdrEz):
(注意:尚不清楚在MSVC中是否安全。Godbolt显示完整的字符串被编码在模板的名称中,但是当我搜索二进制文件时,我没有找到它)
#include <cstdio>
#include <array>
#include <string>
namespace details {
template<size_t N>
struct PastLastSlash {
constexpr PastLastSlash(const char (&s)[N]) {
std::string str{s};
size_t pos = str.rfind('/');
if (pos == std::string::npos) {
pos = 0;
} else {
++pos;
}
size_t len = str.length() - pos + 1;
std::copy(s+pos, s+pos+len, value.data());
}
constexpr size_t len() const {
std::string str{value.data()};
return str.length()+1;
}
std::array<char, N> value{};
};
}
template <details::PastLastSlash S>
struct PastLastSlash {
static constexpr std::array<char, S.len()> copy() {
std::array<char, S.len()> arr;
std::copy(S.value.data(), S.value.data()+S.len(), arr.data());
return arr;
}
static constexpr std::array<char, S.len()> arr{copy()};
static constexpr const char *value = arr.data();
};
int main() {
puts(PastLastSlash<__FILE__>::value);
return 0;
}