使用 memcpy 和 memset 重新分配数组

Reallocate array with memcpy and memset

我接管了一些代码,发现了一个奇怪的数组重新分配。这是数组 class 中的一个函数(由 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 );
    }
}

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

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

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

编辑: 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;
    };
};

其中 ArrayJsonValue 数组的包装器,Stringchar*EmbedStringchar[14]Numberintunsigned intdouble的并集,Details包含它所持有的值的类型。所有值的开头都有16位未使用的数据,用于Details。示例:

struct EmbedString {
    uint16_t : 16;
           char buffer[14] = { 0 };
};

此代码是否具有明确定义的行为基本上取决于两件事:1) JsonValue trivially-copyable 和 2) 如果是这样,一堆全零字节是否为有效对象JsonValue.

的表示

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

我不确定您为什么需要 "prevent calling destructors and moves",但是用零覆盖对象不会阻止 运行 的析构函数。 delete[] values 调用数组成员的解构函数。并且移动一个普通可复制类型数组的元素应该编译成只是复制字节。

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

如果你可以使用 C++17,我建议只使用 std::variant instead of or at least inside this custom JsonValue implementation as that seems to be exactly what it's trying to do. If you need some common information stored in front of whatever the variant value may be, just have a suitable member holding that information in front of the member that holds the variant value rather than relying on every member of the union starting with the same couple of members (which would only be well-defined if all union members are standard-layout types that keep this information in their common initial sequence [class.mem]/23).

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

template <typename T>
struct ZeroingAllocator
{
    using value_type = T;

    T* allocate(std::size_t N)
    {
        return reinterpret_cast<T*>(new unsigned char[N * sizeof(T)]);
    }

    void deallocate(T* buffer, std::size_t N) noexcept
    {
        auto ptr = reinterpret_cast<volatile unsigned char*>(buffer);
        std::fill(ptr, ptr + N, 0);
        delete[] reinterpret_cast<unsigned char*>(buffer);
    }
};

template <typename A, typename B>
bool operator ==(const ZeroingAllocator<A>&, const ZeroingAllocator<B>&) noexcept { return true; }

template <typename A, typename B>
bool operator !=(const ZeroingAllocator<A>&, const ZeroingAllocator<B>&) noexcept { return false; }

然后

using Array = std::vector<JsonValue, ZeroingAllocator<JsonValue>>;

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