C ++编译时子字符串

时间:2019-06-06 06:06:29

标签: c++ c++14 c++17

我的代码库很大,它广泛使用__FILE__进行日志记录。但是,它包含完整路径,不需要(1),(2)可能会违反安全性。

我正在尝试编写编译时子字符串表达式。最终以this solution

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 :按功能https://godbolt.org/z/tAU4j7修剪的叮当声

UPD2:看起来可以使用函数在编译时进行修整,但是完整的字符串将以二进制形式出现。

2 个答案:

答案 0 :(得分:5)

您可以使用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");

compile(godbolt.org)看到它。

对于Windows:

constexpr auto filename(std::wstring_view path)
{ 
    return path.substr(path.find_last_of(L'\\') + 1);
}

答案 1 :(得分:5)

这个想法是创建截断的字符数组,但是它只需要使用编译时功能。通过带有可变字符包的可变参数模板生成数据数组会强制编译器生成与传递的字符串文字没有直接关系的数据。这样,编译器无法使用输入字符串文字,尤其是当此字符串很长时。

带有叮当声的https://godbolt.org/z/WdKNjB

使用msvc进行的Godbolt: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将所有内容组合在一起:

#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。