使用 memcpy 和 memset 重新分配数组

Chr*_*sMM 6 c++ memory-reallocation

我接管了一些代码,并遇到了一个奇怪的数组重新分配。这是一个 Array 类中的函数(由 JsonValue 使用)

void reserve( uint32_t newCapacity ) {
    if ( newCapacity > length + additionalCapacity ) {
        newCapacity = std::min( newCapacity, length + std::numeric_limits<decltype( additionalCapacity )>::max() );
        JsonValue *newPtr = new JsonValue[newCapacity];

        if ( length > 0 ) {
            memcpy( newPtr, values, length * sizeof( JsonValue ) );
            memset( values, 0, length * sizeof( JsonValue ) );
        }

        delete[] values;

        values = newPtr;
        additionalCapacity = uint16_t( newCapacity - length );
    }
}
Run Code Online (Sandbox Code Playgroud)

我明白这一点;它只是分配一个新数组,并将旧数组中的内存内容复制到新数组中,然后将旧数组的内容清零。我也知道这样做是为了防止调用析构函数和移动。

JsonValue是一个带有函数的类,以及一些存储在联合中的数据(字符串、数组、数字等)。

我担心的是这是否实际上是定义的行为。我知道它有效,并且自从我们几个月前开始使用它以来一直没有问题;但如果它未定义,那么并不意味着它会继续工作。

编辑: JsonValue看起来像这样:

struct JsonValue {
// …
    ~JsonValue() {
        switch ( details.type ) {
        case Type::Array:
        case Type::Object:
            array.destroy();
            break;
        case Type::String:
            delete[] string.buffer;
            break;
        default: break;
        }
    }
private:
    struct Details {
        Key key = Key::Unknown;
        Type type = Type::Null; // (0)
    };

    union {
        Array array;
        String string;
        EmbedString embedString;
        Number number;
        Details details;
    };
};
Run Code Online (Sandbox Code Playgroud)

其中Array是围绕阵列的包装纸JsonValueS,Stringchar*EmbedStringchar[14]Number是的并集intunsigned int以及doubleDetails含有它保持值的类型。所有值的开头都有 16 位未使用的数据,用于Details. 例子:

struct EmbedString {
    uint16_t : 16;
           char buffer[14] = { 0 };
};
Run Code Online (Sandbox Code Playgroud)

Mic*_*zel 4

这段代码是否具有明确定义的行为基本上取决于两件事:1) 是JsonValue 可简单复制的,2) 如果是这样,一堆全零字节是否是JsonValue.

\n\n

如果JsonValue是简单可复制的,那么memcpy从一个 s 数组JsonValue到另一个数组确实相当于复制[basic.types]/3上的所有元素。如果全零是 a 的有效对象表示JsonValue,那么memset应该没问题(我相信这实际上属于标准当前措辞的灰色区域,但我相信至少其意图是很好)。

\n\n

我不确定为什么您需要“防止调用析构函数和移动”,但用零覆盖对象并不会阻止析构函数运行。delete[] values 调用数组成员的析构函数。无论如何,移动可简单复制类型的数组的元素应该编译为仅复制字节。

\n\n

此外,我建议摆脱这些StringEmbedString类并简单地使用std::string. 至少,在我看来,唯一的目的EmbedString是手动执行小字符串优化。任何std::string有价值的实现都已经在幕后做到了这一点。请注意,std::string不保证(并且通常不会)可简单复制。因此,您不能简单地替换Stringand EmbedStringwith std::string,同时保留当前实现的其余部分。

\n\n

如果您可以使用 C++17,我建议简单地使用std::variant而不是或至少在这个自定义JsonValue实现中使用,因为这似乎正是它想要做的。如果您需要在变体值之前存储一些通用信息,只需在保存变体值的成员前面放置一个合适的成员来保存该信息,而不是依赖于以相同的一对开始的联合的每个成员成员(只有当所有联合成员都是标准布局类型并将这些信息保留在其公共初始序列[class.mem]/23中时,这才是明确定义的)。

\n\n

的唯一目的Array似乎是作为一个向量,在出于安全原因释放内存之前将其清零。如果是这种情况,我建议只使用std::vector带有分配器的分配器,该分配器在取消分配之前将内存清零。例如:

\n\n
template <typename T>\nstruct ZeroingAllocator\n{\n    using value_type = T;\n\n    T* allocate(std::size_t N)\n    {\n        return reinterpret_cast<T*>(new unsigned char[N * sizeof(T)]);\n    }\n\n    void deallocate(T* buffer, std::size_t N) noexcept\n    {\n        auto ptr = reinterpret_cast<volatile unsigned char*>(buffer);\n        std::fill(ptr, ptr + N, 0);\n        delete[] reinterpret_cast<unsigned char*>(buffer);\n    }\n};\n\ntemplate <typename A, typename B>\nbool operator ==(const ZeroingAllocator<A>&, const ZeroingAllocator<B>&) noexcept { return true; }\n\ntemplate <typename A, typename B>\nbool operator !=(const ZeroingAllocator<A>&, const ZeroingAllocator<B>&) noexcept { return false; }\n
Run Code Online (Sandbox Code Playgroud)\n\n

进而

\n\n
using Array = std::vector<JsonValue, ZeroingAllocator<JsonValue>>;\n
Run Code Online (Sandbox Code Playgroud)\n\n

注意:我通过填充内存来volatile unsigned char*防止编译器优化归零。如果需要支持过度对齐类型,可以将new[]and替换delete[]为直接调用::operator newand ::operator delete(这样做将阻止编译器优化分配)。在 C++17 之前,您必须分配足够大的缓冲区,然后手动对齐指针,例如使用std::align\xe2\x80\xa6

\n