使用 C++17 及更早版本进行编译时字符串压缩

Compile-time string compression with C++17 and earlier

提问人:Fred Helmers 提问时间:11/14/2023 最后编辑:Fred Helmers 更新时间:11/15/2023 访问量:163

问:

我有一个应用程序,它使用带有长链重复字符的字符串。我想将它们以压缩/混淆的形式添加到二进制文件中。为了简单起见,我目前正在使用修改后的 RLE 算法。

我正在使用以下适用于 C++20 的算法。不幸的是,出于商业原因,现在我也必须支持 C++17。我目前对 C++17 的解决方案是将字符串放在 YAML 文件上,并在构建时生成相应的 .cpp“压缩”文件,然后将其链接到进程中。

做一些研究,我发现这个解决方案适用于霍夫曼,但仅支持 C++20(及更高版本)。

我也见过这个解决方案,但“压缩”数据的大小与原始数据相同。

那么问题来了,我怎样才能用C++17重写以下算法?

#include <cstdint>
#include <algorithm>
#include <iostream>
#include <array>
#include <span>
#include <sstream>

struct Array {
    const char* data;
    std::size_t size;
};

constexpr std::size_t compress( const char* data, std::size_t size, char* buf ) {
        if ( size==0 ) return 0;
        std::size_t offset = 0;
        char lastch = *data;
        std::size_t counter = 0;
        auto push = [&]() {
            if ( counter <= 3 ) {
                for ( int j=0; j<counter; ++j ) buf[offset++] = lastch;
            }
            else {
                buf[offset++] = 0;
                buf[offset++] = lastch;
                buf[offset++] = counter;
            }
            counter = 0;
        };
        lastch = data[0];
        counter = 1;
        for ( std::size_t j=1; j<size; ++j ) {
            if ( (data[j]!=lastch) || (counter==255) ) {
                push(); 
                lastch = data[j];
            }
            counter++;
        }
        push();        
        return offset;
}

template< std::size_t N > 
struct RawContainer {
    char raw_data[N];
    constexpr RawContainer( const char (&s)[N] ) {
        std::copy(s,s+N,raw_data);
    }
    constexpr operator const char* () const noexcept {
        return data;
    }
    constexpr auto data() const noexcept {
        return raw_data;
    }
    constexpr auto size() const noexcept {
        return N;
    }
};

template< auto Container >
struct StringCompressor {
    StringCompressor() noexcept {
        compress(Container.data(),Container.size(),compressed_data.data());
    }
    constexpr static auto build_size() noexcept {
        char out[Container.size()*3];
        return compress(Container.data(),Container.size(),out);
    }
    std::string str() noexcept {
        std::ostringstream out;
        out << compressed_data.size() << ": ";
        for ( std::size_t j=0; j<compressed_data.size(); ++j ) {
            out << (int)compressed_data[j] << " ";
        }
        return out.str();
    }
    std::array<char,build_size()> compressed_data;
};

template<RawContainer str>
constexpr StringCompressor<str> operator ""_x() noexcept
{
    return StringCompressor<str>();
}



auto value = "aaaabbbbbbbbbbbbbbbbbbbc"_x;

int main() {
    std::cout << value.str() << std::endl;
}

Godbolt:https://godbolt.org/z/Eh9fMxW75

注意:为简单起见,未包含解压缩算法。

++ 的 C++17

评论

1赞 Yakk - Adam Nevraumont 11/14/2023
我的意思是,它必须有多漂亮?godbolt.org/z/8deo5jErv
1赞 Peter - Reinstate Monica 11/14/2023
啊,好吧,对不起,如果我太慢了,但我理解正确吗:因为您的类型转换(包括压缩)是在编译时完成的,所以对象文件根本不包含未压缩的字符串文字。这使得目标文件(以及库或可执行文件)变小,并防止字符串至少以文字形式显示在生成的二进制文件中,因此您提到的大小减小。
2赞 Peter - Reinstate Monica 11/14/2023
换言之,您将编译器用作脚本引擎。您可以使用其他更具表现力的脚本引擎;-)。
1赞 Peter - Reinstate Monica 11/14/2023
顺便说一句,我注意到这个字符串仍然出现在 godbolt 的拆卸中,第 418 行。我想这意味着从程序映像中消除字符串常量的尝试在大小和信息泄漏方面都没有成功?
1赞 Fred Helmers 11/14/2023
@Peter-ReinstateMonica 是的,我想这也是所有元模板编程的故事。99% 的 TMP 可以替换为 Jinja2 模板。

答:

1赞 Peter - Reinstate Monica 11/15/2023 #1

可能在这里有一个 C++17 示例,它的额外好处是它不将字符串文字存储在二进制文件中。我检查了相当长的参数(汇编程序中第 35 行 pp. 中的字符串是压缩结果)。

与代码中不同,字符串文字用于初始化 constexpr 数组或指针变量,然后将其提供给简化的压缩器类。大小必须单独计算。每个字符串的代码更多,但文字仍然只出现一次。从外观上看,它可以很容易地生成。

调用端如下所示:

        constexpr const char p[] = "aaaabbbbbbbbbbbbbbbbbbbc";
        constexpr std::size_t comprSz = compress(p, sizeof(p), nullptr);
        constexpr Compressor<comprSz> cpr{p, sizeof(p)};
        print(cpr.comprData, comprSz);

可以将大小计算移动到另一个中间类中,但无论出于何种原因,似乎都不可能使用普通字符串文字作为模板参数,以便调用可以是单个语句,就像使用用户文字一样。

1赞 n. m. could be an AI 11/15/2023 #2

这是我的版本。该函数是从您的代码中复制的,没有更改,其余部分从我的评论中移除,并进行了一些琐碎的修改。用法很简单:compress

COMPRESSED_LITERAL("aaaabbbbbbbbbbbbbc")

如您所见,原始字符串和压缩代码都不会在生成的对象中留下任何痕迹。

我没有实现,因为压缩的字符串在生成的程序集中是可见的。我让压缩版本和未压缩版本都输出到只是为了并排比较生成的程序集,尽管压缩版本当然不会输出任何内容,因为开始时嵌入了零。str()std::cout

我能想到的唯一缺点是您需要使用宏。