C++ Logo

std-proposals

Advanced search

[std-proposals] Fwd: Standardised Type Punning API for Wrapper Types

From: Josh Warren <xzaton.jw_at_[hidden]>
Date: Mon, 2 Sep 2024 13:38:53 +0100
Hi std proposals,

The Problem:

It is common to "strongly type" PoD in C++ by wrapping a single value in a
class/struct, therefore providing the compiler with a richer context for
identifying misuse of the data stored therein. A good example of this is
`std::chrono` which strongly types time related quantities to prevent
logical errors which would otherwise not be caught at compile-time (e.g.
conversions between seconds and minutes). However, this strong typing can
become cumbersome when interfacing with code that doesn't use the same
"strong type" for logically equivalent data. Mechanisms/backdoors exist to
help circumvent this, such as `.count()` on `std::chrono::duration<>`
types, but this a specialised solution, since there is no standard for how
all such wrapper types should handle this issue. Also, that approach fails
in the case of aggregate types, and we are forced to invoke UB. e.g.:

#include <chrono>

void old_c_api(long* times, unsigned long count)
{
    for (int i = 0; i < count; ++i)
    {
        printf("%ld", times[i]);
    }
}

int main()
{
    {
        using namespace std::chrono_literals;
        std::vector times{1s, 2s, 3s};
        old_c_api(reinterpret_cast<long*>(times.data()),
times.size()); // UB, but do we have enough information for this to
not need to be?
    }

    {
        std::vector times{4L, 5L, 6L};
        old_c_api(times.data(), times.size());
    }
}

Which prints the expected: 123456

Proposed Solution:

Introduce a new way of defining an explicit conversion operator which
signals to the compiler that we would like the wrapper type to support type
punning to the type of the stored data.

#include <utility>

struct my_distance_type
{
    double value;
    explicit operator double() = default; // if the compiler can
generate this, then type punning is allowed
};

void old_c_api(double* distance)
{
    // ...
}

int main()
{
    my_distance_type d{.value = 1.};
    double* d_ptr = &d; // check if type punning to the requested type
is supported, if it is, treat d as a double
    old_c_api(d); // still not allowed since d cannot be implicitly
converted to double, so we don't give up the strong typing as the
default
    old_c_api(std::to_underlying(&d)); // add template specialisation
for non-enum types that provide a compiler-generated explicit
conversion operator
}

Then it would be possible to introduce type punning for aggregate types:

#include <utility>
#include <vector>
#include <iostream>

struct my_distance_type
{
    double value;
    explicit operator double() = default; // if the compiler can
generate this, then type punning is allowed
};


void cpp_api(std::vector<double>& distances)
{
    for (auto element : distances)
    {
        std::cout << element << std::endl;
    }
}

int main()
{
    std::vector<my_distance_type> d1;
    d1.emplace_back(1);
    d1.emplace_back(2);
    d1.emplace_back(3);
    cpp_api(reinterpret_cast<std::vector<double>&>(d1)); // UB again
    cpp_api(std::to_underlying(d1)); // if the value_types can be type
punned, then this is equivalent to the line above without UB
}

The predicates for the explicit defaulted conversion operator would include:

   - std::is_trivially_destructible_v<wrapper<T>>
   - wrapper<T> must define a single data member of type T (don't think
   this is possible with <type_traits>)

Any feedback is welcome, and thank you for reading this

Josh

Received on 2024-09-02 12:39:09