Examples of Undefined Behavior Related to Type Accessibility in C++

From C++ standard:

Attempting to read or modify the stored value through any other type results in undefined behavior.

The first basic example illustrating this principle:

#include <cstdint>

std::uint32_t toInt(float value)
{
    auto* p = reinterpret_cast<std::uint32_t*>(&value); // The cast itself is permitted.
    return *p; // Undefined behavior: accesses a float object through a std::uint32_t glvalue.
}

An example demonstrating what we do in develop branch in our project:

#include <cstddef>
#include <cstdint>

struct Packet
{
    std::int8_t x;
    std::int16_t y;
    std::int32_t z;
};

struct A
{
    float value;
};

static_assert(sizeof(A) < sizeof(Packet));
static_assert(alignof(A) <= alignof(Packet));

A getA(const Packet* packet)
{
    const auto* bytes = reinterpret_cast<const std::byte*>(packet);
    // Allowed: a Packet's object representation may be inspected through std::byte.

    const auto* a = reinterpret_cast<const A*>(bytes);
    // The cast itself is allowed, but it does not create an A object.

    return *a;
    // Undefined behavior: no A object exists at this address.
    // Casting through std::byte does not start the lifetime of an A object.
}

An example demonstrating how we tried to fix this:

#include <cstddef>
#include <cstdint>
#include <new>

struct Packet
{
    std::int8_t x;
    std::int16_t y;
    std::int32_t z;
};

struct A
{
    float value;
};

static_assert(sizeof(A) < sizeof(Packet));
static_assert(alignof(A) <= alignof(Packet));

A toA(const Packet* packet)
{
    auto* mutablePacket = const_cast<Packet*>(packet);
    // The const_cast itself is allowed because the original object in main
    // is not actually const.

    auto* bytes = reinterpret_cast<std::byte*>(mutablePacket);
    // This conversion only produces a pointer to the Packet's storage.

    auto* a = ::new (bytes) A;
    // Placement new creates an A object and starts its lifetime.
    // Reusing the storage ends the lifetime of the entire Packet object.
    // A is default-initialized, but its trivial default constructor does
    // not initialize A::value, so value remains indeterminate.

    return *a;
    // Undefined behavior: copying *a reads the indeterminate float value.
}

int main()
{
    Packet packet{1, 2, 3}; // The original object is not const.

    const A a = toA(&packet);
    // Undefined behavior occurs inside toA when the indeterminate
    // A::value member is read while constructing the returned A object.

    // Accessing packet here would also be undefined behavior because
    // its lifetime ended when the A object was created in its storage.

    return 0;
}

So basically we did this:

int main()
{
    int* value = new int; // Default-initialized; the value is indeterminate.
    return *value;        // Undefined behavior: reads an indeterminate int.
}

From C++ 23 standard:

If an indeterminate value is produced by an evaluation, the behavior is undefined …

1 Response to Examples of Undefined Behavior Related to Type Accessibility in C++

  1. dmitriano says:

    An example of placement new usage in a similar scenario: https://stackoverflow.com/questions/79998366/using-stdstart-lifetime-as-multiple-times-with-the-same-source-object
    It does std::memcpy after placement new.

Leave a Reply

Your email address will not be published. Required fields are marked *