Search code examples
c++c++20placement-newbit-cast

Can i use placement new as a bit_cast?


I'm interested in ways to get around the restriction on binary type conversion, because bit_cast uses copying to a variable on the stack, which is not very fast. But as far as I know, trivial types do not initialize memory in default constructors. Does it mean that for bit_casting into a trivial type we can use placement new with a pointer to the memory we are interested in and thus keep bytes in memory unchanged and save ourselves from unnecessary copying? Are there any positions of the standard on this issue that I can't find and that forbid me to use placement new with some "non-thematic" pointers?

For example, the first option is how others use this feature. The second option is how I want to use it.

#include <iostream>
using namespace std;
class Foo 
{ 
    private: int __unused0 : 31; //A little bit of binary magic
    private: int __unused1 :  1; 
    public: Foo() : __unused0(1), __unused1(1) {} //We're still saving trivially_copyable, so we're good
};

int main()
{
    //char bytes[1024];
    //auto* foo = new(bytes) Foo;
    
    Foo bytes{};
    auto* foo = new(&bytes) int;
    
    cout << *foo;
    return 0;
}

UPD: The purpose of all this, dealing with raw bytes in memory without changing the container. In the simplest case, I need mini-optimizations like "check the high bit in a register", which is achieved with a single assembly command like [x < 0] while working with bit fields in structures creates a sequence of commands like [x & 0x80][x >> 7][x == 0].

UPD2: I am interested in the probable possibility of using placement new in the code, because from the point of view of semantics. When using bit_cast we copy one object into another, and when using new we use the memory of object A as an initializer of object B. Both expressions are well optimized by modern compilers, but the point of the question is not whether it makes sense to write this way, but whether the standard is not violated when I write an expression based on placement new. +I am aware that if we write something into object B I will get undefined behavior when using type A. This is quite logical.


Solution

  • You are describing something similar to what std::start_lifetime_as does in C++23.

    Placement new "does not keep the object representation" (as it says here), which means that reading *foo is undefined behaviour since the int object hasn't been initialized. std::start_lifetime_as implicitly creates an int object with the same object representation and returns a pointer to it:

    auto* foo = std::start_lifetime_as<int>(&bytes);
    
    // A C++17 implementation
    template<class T>
    T* start_lifetime_as(void* p) noexcept {
        // static_assert(std::is_implicit_lifetime_v<T> && std::is_trivially_copyable_v<T>);
        static_assert(std::is_scalar_v<T> || std::is_array_v<T> || (std::is_class_v<T> && std::is_trivially_destructible_v<T> && std::is_trivially_copyable_v<T>));
    
        std::byte object_representation[sizeof(T)];
        std::memcpy(object_representation, p, sizeof(T));
        void* result = ::new (p) std::byte[sizeof(T)];
        std::memcpy(result, object_representation, sizeof(T));  
        return std::launder(reinterpret_cast<T*>(result));
    }
    

    The C++17 implementation follows the process described in P2590R2 in section 1.2. You might even be able to get away with:

    template<class T>
    T* start_lifetime_as(void* p) noexcept {
        std::memmove(p, p, sizeof(T));  // Compiler might be able to optimize this better than two memcpys
        // return pointer to the implicitly created object
        return std::launder(reinterpret_cast<T*>(p));
    }