Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constexpr alternative to placement new to be able to leave objects in memory uninitialized?

Tags:

c++

c++17

I am trying to create a static container which has stack based memory and can hold N instances of T. Much alike std::vector I want currently unused memory to not contain initialized items of T. This is usually solved with placement new but that's not possible to use in constexpr.

Using unions I found a trick that you can use a union for this as follows:

template <typename value_type>
union container_storage_type
{
    struct empty{};
    constexpr container_storage_type(): uninitialized{}{}
    constexpr container_storage_type(value_type v): value(v){}
    constexpr void set(value_type v)
    {
        *this = literal_container_storage_type{v};
    }

    empty uninitialized;
    value_type value;
};

This lets you store items uninitialized by setting the empty member and this works around the limitation that all members in constexpr have to be initialized.

Now the problem with this approach is that if value_typeis a type that implements operator=, the rule for unions says:

If a union contains a non-static data member with a non-trivial special member function (copy/move constructor, copy/move assignment, or destructor), that function is deleted by default in the union and needs to be defined explicitly by the programmer.

This means that to be able to use this trick, I need to implement operator= in the union too, but how would that look?

constexpr container_storage_type& operator=(const container_storage_type& other)
{           
    value = other.value; //ATTEMPT #1
    //*this = container_storage_type(other.value);ATTEMPT #2

    return *this;
}

Attempt #1: This does not seem possible as the compiler complains that changing the active member of a union is simply disallowed in constant expressions. Attempt #2: This works in the set() method from the previous snippet, as it doesn't change the active member per se, but reassigns the whole union. This trick seems unable to be used in the assignment operator however since that causes endless recursion...

Am I missing something here, or is this truly a dead end for using unions as a placement-new alternative in constexpr?

Are there other alternatives to placement new that I have completely missed?

https://godbolt.org/z/km0nTY Code that illustrates the problem

like image 690
Tobias Avatar asked Oct 31 '18 13:10

Tobias


People also ask

What is c++20 constexpr dynamic memory allocation?

constexpr Dynamic Memory Allocation, C++20 constexpr has become a major feature for compile-time programming in C++. Introduced in a simple form in C++11 evolved into almost another “sub-language”, an alternative to regular template code. In C++20 you can even use std::vector and std::string in constexpr context!

What's new with if constexpr in c++20?

For example, you could use tag dispatching or SFINAE. Fortunately, that’s changed, and we can now benefit from if constexpr and concepts from C++20! Let’s see how we can use it and replace some std::enable_if code. Updated in April 2021: C++20 changes - concepts. Updated in August 2022: More if constexpr examples (use case 4).

How does the new-expression allocate memory in C?

The new-expression allocates storage by calling the appropriate allocation function. If type is a non-array type, the name of the function is operator new. If type is an array type, the name of the function is operator new [].

What is placement new in C++?

Placement new is a variation new operator in C++. Normal new operator does two things : (1) Allocates memory (2) Constructs an object in allocated memory. Placement new allows us to separate above two things. In placement new, we can pass a preallocated memory and construct an object in the passed memory. new vs placement new.


1 Answers

In C++17, you can't.

The current restrictions on what you cannot do in constant expressions include:

  • an assignment expression ([expr.ass]) or invocation of an assignment operator ([class.copy.assign]) that would change the active member of a union;

  • a new-expression;

There really is no way around that.


In C++20, you will be able to, but probably not the way you think. The latter restriction is going to be relaxed in C++20 as a result of P0784 to something like:

  • a new-expression (8.3.4), unless the selected allocation function is a replaceable global allocation function (21.6.2.1, 21.6.2.2);

That is, new T will become fine but new (ptr) T will still not be allowed. As part of making std::vector constexpr-friendly, we need to be able to manage "raw" memory - but we still can't actually manage truly raw memory. Everything still has to be typed. Dealing with raw bytes is not going to work.

But std::allocator doesn't entirely deal in raw bytes. allocate(n) gives you a T* and construct takes a T* as a location and a bunch of arguments and creates a new object at that location. You may be wondering at this point how this is any different from placement new - and the only difference is that sticking with std::allocator, we stay in the land of T* - but placement new uses void*. That distinction turns out to be critical.

Unfortunately, this has the interesting consequence of your constexpr version "allocates" memory (but it allocates compiler memory, which will get elevated to static storage as necessary - so this does what you want) - but your pure runtime version surely does not want to allocate memory, indeed the whole point would be that it does not. To that end, you will have to use is_constant_evaluated() to switch between the allocating at constant evaluation time and non-allocating at runtime. This is admittedly not beautiful, but it should work.

like image 134
Barry Avatar answered Oct 17 '22 21:10

Barry