【问题标题】:C++ compile-time substringC++ 编译时子字符串
【发布时间】:2019-06-06 06:06:29
【问题描述】:

我有非常大的代码库,它广泛使用__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修剪的clang

UPD2: 看起来可以使用函数对编译时间进行修整,但完整的字符串会以二进制形式存在。

【问题讨论】:

  • 我无法重现“这很好用,我检查了汇编代码,行在编译时被修剪,只有文件名存在于二进制文件中。”使用最新的 GCC 和 clang:godbolt.org/z/tFR_2D 此外,在 MSCV 中使用 /Ox,即使对于原始 past_last_slash(__FILE__),也可以看到完整路径:godbolt.org/z/B1M1nR
  • 另外,这个宏似乎不是有效的 C++ 代码,但 gcc 扩展:gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html--pedantic gcc 拒绝它。
  • 嗯,奇怪,godbolt 未能在 MSVC 的编译时做到这一点......我将尝试提供一些工作示例。
  • 您的 Godbolt 演示显示完整的字符串 ("./example.cpp") 仍存储在可执行文件中。编译器只是在编译时计算一个偏移量 (mov edi, offset .L.str+2)。
  • 在很久以前,当我关心这个时,我传递了一个备用文件名'-D__ALTNAME__="1234.cpp"',然后在#includes 之后立即放置#line __LINE__ __ALTNAME__。请注意,这会使编译器在 1234.cpp 中报告错误。

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


【解决方案1】:

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

Godbolt with 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&lt;3,7&gt; == Seq&lt;3,4,5,6,7&gt;。我们仍然需要一些东西来将选定的字符存储在数组中,但是使用编译时表示,这是带有字符的可变参数模板参数:

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 进行构建。

【讨论】:

  • 太棒了!它不以二进制形式存储完整的文件路径,只存储实际使用的文件名。非常感谢
  • @yudjin 以前的版本没有强制编译器生成编译时数组。检查当前答案(也适用于 msvc)。
【解决方案2】:

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

【讨论】:

  • 正如问题所问,这在 C++14 中不起作用。对于 C++20 有更好的解决方案。
  • @jkflying。它在 C++17 中工作,这也是 OP 的标签。
【解决方案3】:

使用 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,应该可以将 __FILE__ 直接传递给模板,而不需要那些 static constexpr 变量

【讨论】:

    猜你喜欢
    • 2011-04-12
    • 2013-06-25
    • 1970-01-01
    • 2011-05-05
    • 2010-12-18
    • 1970-01-01
    • 2011-06-25
    • 2011-11-08
    • 1970-01-01
    相关资源
    最近更新 更多