I've taken over some code, and came across a weird reallocation of an array. This is a function from within an Array class (used by the 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 );
}
}
I get the point of this; it is just allocating a new array, and doing a copy of the memory contents from the old array into the new array, then zero-ing out the old array's contents. I also know this was done in order to prevent calling destructors, and moves.
The JsonValue is a class with functions, and some data which is stored in a union (string, array, number, etc.).
My concern is whether this is actually defined behaviour or not. I know it works, and has not had a problem since we began using it a few months ago; but if its undefined then it doesn't mean it is going to keep working.
EDIT:
JsonValue looks something like this:
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;
};
};
Where Array is a wrapper around an array of JsonValues, String is a char*, EmbedString is char[14], Number is a union of int, unsigned int, and double, Details contains the type of value it holds. All values have 16-bits of unused data at the beginning, which is used for Details. Example:
struct EmbedString {
uint16_t : 16;
char buffer[14] = { 0 };
};
Whether this code has well-defined behavior basically depends on two things: 1) is
JsonValuetrivially-copyable and, 2) if so, are a bunch of all-zero Bytes a valid object representation for aJsonValue.If
JsonValueis trivially-copyable, then thememcpyfrom one array ofJsonValues to another will indeed be equivalent to copying all the elements over [basic.types]/3. If all-zeroes is a valid object representation for aJsonValue, then thememsetshould be ok (I believe this actually falls into a bit of a grey-area with the current wording of the standard, but I believe at least the intention would be that this is fine).I'm not sure why you'd need to "prevent calling destructors and moves", but overwriting objects with zeroes does not prevent destructors from running.
delete[] valueswill call the destructurs of the array members. And moving the elements of an array of trivially-copyable type should compile down to just copying over the bytes anyways.Furthermore, I would suggest to get rid of these
StringandEmbedStringclasses and simply usestd::string. At least, it would seem to me that the sole purpose ofEmbedStringis to manually perform small string optimization. Anystd::stringimplementation worth its salt is already going to do exactly that under the hood. Note thatstd::stringis not guaranteed (and will often not be) trivially-copyable. Thus, you cannot simply replaceStringandEmbedStringwithstd::stringwhile keeping the rest of this current implementation.If you can use C++17, I would suggest to simply use
std::variantinstead of or at least inside this customJsonValueimplementation 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).The sole purpose of
Arraywould seem to be to serve as a vector that zeroes memory before deallocating it for security reasons. If this is the case, I would suggest to just use anstd::vectorwith an allocator that zeros memory before deallocating instead. For example:and then
Note: I fill the memory via
volatile unsigned char*to prevent the compiler from optimizing away the zeroing. If you need to support overaligned types, you can replace thenew[]anddelete[]with direct calls to::operator newand::operator delete(doing this will prevent the compiler from optimizing away allocations). Pre C++17, you will have to allocate a sufficiently large buffer and then manually align the pointer, e.g., usingstd::align…