获取 C++ 的字节表示 class

Get byte representation of C++ class

我有需要用 SHA256 散列的对象。该对象有如下几个字段:

class Foo {
    // some methods
    protected:
       std::array<32,int> x;
       char y[32];
       long z;
}

有没有一种方法可以像访问结构一样直接访问代表内存中 3 个成员变量的字节?这些散列需要尽快计算,所以我想避免 malloc'ing 一组新的字节并复制到堆分配的数组。或者是简单地将结构嵌入 class?

中的答案

获得这些变量的精确二进制表示是至关重要的,这样 SHA256 就可以完全相同,因为这 3 个变量是相等的(所以我不能有任何额外的填充字节等包含在哈希函数)

只需将指向对象的指针转换为指向字符的指针。您可以按增量迭代字节。使用 sizeof(foo) 检查溢出。

您可以通过制作一个知道您的成员变量布局的迭代器来解决这个问题。制作 Foo::begin()Foo::end() 函数,您甚至可以利用基于范围的 for 循环。

如果你可以增加它并取消引用它,你可以在任何其他可以使用 LegacyForwardIterator.

的地方使用它

添加比较函数以访问常见的 it = X.begin(); it != X.end(); ++it 习语。

一些缺点包括:丑陋的库代码、较差的可维护性以及(在当前形式中)不考虑字节顺序。

后一个缺点的解决方案留作 reader 的练习。

#include <array>
#include <iostream>

class Foo {
    friend class FooByteIter;

public:
    FooByteIter begin() const;

    FooByteIter end() const;

    Foo(const std::array<int, 2>& x, const char (&y)[2], long z)
    : x_{x}
    , y_{y[0], y[1]}
    , z_{z}
    {}

protected:
    std::array<int, 2> x_;
    char y_[2];
    long z_;
};

class FooByteIter {
public:
    FooByteIter(const Foo& foo)
        : ptr_{reinterpret_cast<const char*>(&(foo.x_))}
        , x_end_{reinterpret_cast<const char*>(&(foo.x_)) + sizeof(foo.x_)}
        , y_begin_{reinterpret_cast<const char*>(&(foo.y_))}
        , y_end_{reinterpret_cast<const char*>(&(foo.y_)) + sizeof(foo.y_)}
        , z_begin_{reinterpret_cast<const char*>(&(foo.z_))}
    {}

    static FooByteIter end(const Foo& foo) {
        FooByteIter fbi{foo};
        fbi.ptr_ = reinterpret_cast<const char*>(&foo.z_) + sizeof(foo.z_);

        return fbi;
    }

    bool operator==(const FooByteIter& other) const { return ptr_ == other.ptr_; }
    bool operator!=(const FooByteIter& other) const { return ! (*this == other); }

    FooByteIter& operator++() {
        ptr_++;
        if (ptr_ == x_end_) {
            ptr_ = y_begin_;
        }
        else if (ptr_ == y_end_) {
            ptr_ = z_begin_;
        }

        return *this;
    }

    FooByteIter operator++(int) {
        FooByteIter pre = *this;
        (*this)++;
        return pre;
    }

    char operator*() const {
        return *ptr_;
    }

private:
    const char* ptr_;

    const char* const x_end_;
    const char* const y_begin_;
    const char* const y_end_;
    const char* const z_begin_;
};

FooByteIter Foo::begin() const {
    return FooByteIter(*this);
}

FooByteIter Foo::end() const {
    return FooByteIter::end(*this);
}

template <typename InputIt>
char checksum(InputIt first, InputIt last) {
    char check = 0;
    while (first != last) {
        check += (*first);
        ++first;
    }

    return check;
}

int main() {
    Foo f{{1, 2}, {3, 4}, 5};
    for (const auto b : f) {
        std::cout << (int)b << ' ';
    }

    std::cout << std::endl;

    std::cout << "Checksum is: " << (int)checksum(f.begin(), f.end()) << std::endl;
}

您可以通过为您可能关心的所有数据类型制作序列化函数来进一步概括这一点,允许对 类 不是普通旧数据类型的序列化。

警告

此代码假定被序列化的基础类型本身没有内部填充。 This 答案适用于 this 数据类型,因为它由本身不填充的类型组成。要使此方法适用于具有填充数据类型的数据类型,需要一直向下递归此方法。

大多数哈希 类 能够在返回哈希之前获取多个区域,例如如:

class Hash {
    public:
        void update(const void *data, size_t size) = 0;
        std::vector<uint8_t> digest() = 0;
}

因此您的散列方法可能如下所示:

std::vector<uint8_t> Foo::hash(Hash *hash) const {
    hash->update(&x, sizeof(x));
    hash->update(&y, sizeof(y));
    hash->update(&z, sizeof(z));
    return hash->digest();
}

只要您能够使您的 class 成为 aggregate,即 std::is_aggregate_v<T> == true,您实际上可以在某种程度上反映结构的成员。

这使您可以轻松地对成员进行哈希处理,而无需实际命名它们。 (你也不必在每次添加新成员时都记住更新哈希函数)

第 1 步:获取聚合中的成员数

首先我们需要知道给定聚合类型有多少成员。
我们可以通过(ab-)使用 aggregate initialization.

来检查这一点

示例:
给定 struct Foo { int i; int j; };:

Foo a{}; // ok
Foo b{{}}; // ok
Foo c{{}, {}}; // ok
Foo d{{}, {}, {}}; // error: too many initializers for 'Foo'

我们可以使用它来获取结构内的成员数量,方法是尝试添加更多初始化程序直到出现错误:


template<class T>
concept aggregate = std::is_aggregate_v<T>;

struct any_type {
    template<class T>
    operator T() {}
};

template<aggregate T>
consteval std::size_t count_members(auto ...members) {
    if constexpr (requires { T{ {members}... }; } == false)
        return sizeof...(members) - 1;
    else
        return count_members<T>(members..., any_type{});
}

请注意,我使用 {members}... 而不是 members...
这是因为数组 - 像 struct Bar{int i[2];}; 这样的结构可以用 2 个元素初始化,例如Bar b{1, 2},所以如果我们使用 members....

,我们的函数会为 Bar 返回 2

第 2 步:提取成员

现在我们知道我们的结构有多少成员,我们可以使用 structured bindings 来提取它们。

遗憾的是,当前标准无法创建具有可变数量表达式的结构化绑定表达式,因此我们必须为每个要支持的额外成员添加几行额外的代码。

对于此示例,我最多只添加了 4 个成员,但您可以根据需要添加任意数量的成员:

template<aggregate T>
constexpr auto tie_struct(T const& data) {
    constexpr std::size_t fieldCount = count_members<T>();
    if constexpr(fieldCount == 0) {
        return std::tie();
    } else if constexpr (fieldCount == 1) {
        auto const& [m1] = data;
        return std::tie(m1);
    } else if constexpr (fieldCount == 2) {
        auto const& [m1, m2] = data;
        return std::tie(m1, m2);
    } else if constexpr (fieldCount == 3) {
        auto const& [m1, m2, m3] = data;
        return std::tie(m1, m2, m3);
    } else if constexpr (fieldCount == 4) {
        auto const& [m1, m2, m3, m4] = data;
        return std::tie(m1, m2, m3, m4);
    } else {
        static_assert(fieldCount!=fieldCount, "Too many fields for tie_struct! add more if statements!");
    }
}

static_assert 中的 fieldCount!=fieldCount 是有意的,这可以防止编译器过早地评估它(它只会在 else 实际命中时才会抱怨)

现在我们有一个函数可以为我们提供对任意聚合的每个成员的引用。

示例:

struct Foo {int i; float j; std::string s; };

Foo f{1, 2, "miau"};
// tup is of type std::tuple<int const&, float const&, std::string const&>
auto tup = tie_struct(f);

// this will output "12miau"
std::cout << std::get<0>(tup) << std::get<1>(tup) << std::get<2>(tup) << std::endl;

第 3 步:散列成员

现在我们可以将任何聚合转换为其成员的元组,散列它应该不是一个大问题。

您基本上可以随意散列各个类型,然后组合各个散列:

// for merging two hash values
std::size_t hash_combine(std::size_t h1, std::size_t h2)
{
    return (h2 + 0x9e3779b9 + (h1<<6) + (h1>>2)) ^ h1;
}

// Handling primitives
template <class T, class = void>
struct is_std_hashable : std::false_type { };

template <class T>
struct is_std_hashable<T, std::void_t<decltype(std::declval<std::hash<T>>()(std::declval<T>()))>> : std::true_type { };

template <class T>
concept std_hashable = is_std_hashable<T>::value; 

template<std_hashable T>
std::size_t hash(T value) {
    return std::hash<T>{}(value);
}

// Handling tuples
template<class... Members>
std::size_t hash(std::tuple<Members...> const& tuple) {
    return std::apply([](auto const&... members) {
        std::size_t result = 0;
        ((result = hash_combine(result, hash(members))), ...);
        return result;
    }, tuple);
}

template<class T, std::size_t I>
using Arr = T[I];

// Handling arrays
template<class T, std::size_t I>
std::size_t hash(Arr<T, I> const& arr) {
    std::size_t result = 0;
    for(T const& elem : arr) {
        std::size_t h = hash(elem);
        result = hash_combine(result, h);
    }
    return result;
};

// Handling structs
template<aggregate T>
std::size_t hash(T const& agg) {
    return hash(tie_struct(agg));
}

这允许您基本上散列任何聚合结构,即使是数组和嵌套结构:

struct Foo{ int i; double d; std::string s; };
struct Bar { Foo k[10]; float f; };

std::cout << hash(Foo{1, 1.2f, "miau"}) << std::endl;
std::cout << hash(Bar{}) << std::endl;

full example on godbolt

脚注

  • 这仅适用于聚合
  • 无需担心填充问题,因为我们直接访问成员。
  • 如果您需要超过 4 个成员,您必须在 tie_struct 中再添加几个 if
  • 所提供的 hash() 函数无法处理所有类型 - 如果您需要,例如std::arraystd::pair 等...您需要为它们添加重载。
  • 它有很多样板代码,但它非常强大。
  • 如果允许使用 boost
  • ,您也可以将 Boost.PFR 用于聚合到元组部分