0

I'm looking for a class that is similar to std::optional, but without the internal flag which tells whether the container is empty or not. I want to be able to declare a variable of type T without invoking T' constructor, and later on move or emplace something into it on my discretion. Specifically I want to work with non-default-constructible T's.

It can be achieved easily with std::optional, but it comes with an overhead of the internal flag. I want this wrapper's size to be equal to sizeof(T).

I know such a class can be implemented using placement new (as are std::optional, std::variant etc). But it looks like a lot of work, and I'm wondering if something like that already exists...

ciamej
  • 6,918
  • 2
  • 29
  • 39
  • 1
    Sounds like a `union`? `template union foo { char dummy; T data; };` – Ted Lyngmo Feb 12 '23 at 17:55
  • @TedLyngmo What is `dummy` for? – HolyBlackCat Feb 12 '23 at 17:56
  • @HolyBlackCat For the non-default-constructible `T`s :) – Ted Lyngmo Feb 12 '23 at 17:57
  • @TedLyngmo [It doesn't seem to help](https://gcc.godbolt.org/z/r4f1zzv6E). Something like this is needed: `template union foo {T data; foo(){} ~foo(){}};`, – HolyBlackCat Feb 12 '23 at 17:58
  • @HolyBlackCat Yes, you are right, it was a little hasty – Ted Lyngmo Feb 12 '23 at 17:59
  • Consider using [`std::aligned_storage`](https://en.cppreference.com/w/cpp/types/aligned_storage) long with [`std::allocator`](https://en.cppreference.com/w/cpp/memory/allocator/construct) to construct/destruct the object in that storage. Usually it's just better overall to make all types default constructible, even if the default constructor results in an unusable but valid instance (like a moved-from instance). – François Andrieux Feb 12 '23 at 18:08
  • @HolyBlackCat Ok, but how do I emplace T inside foo? – ciamej Feb 12 '23 at 18:08
  • Do I cast &foo to void*? And use it in placement new? – ciamej Feb 12 '23 at 18:09
  • 1
    `::new((void *)&foo.data) Type(value);`. Without the cast is ok too, but this form makes it non-overridable. Or [`std::construct_at`](https://en.cppreference.com/w/cpp/memory/construct_at), which does the same thing. – HolyBlackCat Feb 12 '23 at 18:15
  • 1
    Be sure to thoroughly test this to make sure you always match constructors/destructors, and don't leak stuff if something throws. – HolyBlackCat Feb 12 '23 at 18:16
  • 1
    I agree -- very often defeating C++'s type safety -- a core, fundamental part of C++ -- is often "a lot of work", but cannot be avoided. – Sam Varshavchik Feb 12 '23 at 18:26
  • You'll need to use placement new, and in-place destructor, and make sure you don't use the T until it is placement new'd and not use it after it is in-place destructed. Which to seems like `std::optional` internal flag isn't much overhead, but I presume that your use-case it is too much overhead. – Eljay Feb 12 '23 at 21:18

1 Answers1

2

There is nothing for it in the standard library, but it is relatively straight-forward to write such an unsafe optional as a union class. It still requires that you implement the constructor and methods with a placement-new (or construct_at).

However, such a class can't follow the RAII principle properly, because the destructor cannot assume that the unsafe optional is non-empty, so that it can't destroy the contained object. Instead the user of the unsafe optional has to manually choose to destruct the contained object before the unsafe optional's lifetime ends or before a new object is emplaced into it.

It would be preferably to rewrite the user code so that it isn't necessary to construct the empty unsafe optional first. The user code must know whether it contains an object anyway for the reason above, so it should always be possible. (I don't know your concrete use case, so I can't give concrete advice.)


From your comment it seems like you are writing a container. A container can use the standard Allocator concept together with std::allocator_traits as all the standard library allocator-aware containers (e.g. std::vector, std::map, etc.) do:

Your class takes a template parameter called A, usually defaulted to std::allocator<T> (the default allocator using operator new/operator delete), then define

using Alloc = typename std::allocator_traits<A>::template rebind<T>;

and store an instance alloc of Alloc as the allocator (possibly passed through a constructor or default-constructed).

Then to obtain memory you do

T* storage = std::allocator_traits<Alloc>::allocate(alloc, n);

where n is the number of elements to allocate memory for, without constructing any object.

Then to construct the i's object you do

std::allocator_traits<Alloc>::construct(alloc, &storage[i], /*constructor args*/);

To destruct the object you do

std::allocator_traits<Alloc>::destroy(alloc, &storage[i]);

and to deallocate the memory you do

std::allocator_traits<Alloc>::deallocate(alloc, n);

where n must be the same as the allocation size.

That way your container will automatically support all classes as allocator that follow the standard's Allocator concept and no dangerous casts or anything like that is required.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • I'm implementing a bounded queue, and I have an arrays of cells where each cell is a seqNum and T object. It works alright but not for non-default-constructible T's. What I would like to do is to have non-initialized T inside cells, and during enqueue move the data into cells (or emplace). And then on dequeue I would move data out, and manually invoke destructor. – ciamej Feb 12 '23 at 18:12
  • @ciamej In that case you only need to use placement-new (or `std::construct_at`) and explicit destructor calls (or `std::destroy_at`). That's basically already as easy as possible. If you use the `std::allocator_traits` interface, then you don't even need to explicitly do any unsafe casts or anything like that. – user17732522 Feb 12 '23 at 18:14
  • Ok, thank you. I haven't heard about std::construct_at yet. It looks like the tool I need. – ciamej Feb 12 '23 at 18:16
  • How would you avoid reinterpret_cast with std::allocator_traits? (That's also something I'm not familiar with yet) – ciamej Feb 12 '23 at 18:17
  • Just to clarify, you regard the queue as the container, not the individual cells? This is where I got confused, because I don't store the T elements in a contiguous memory block, but rather interleaved with std::atomic. – ciamej Feb 12 '23 at 18:29
  • 1
    @ciamej I assume that the whole queue is the container. You can (probably) still use the allocator interface for what you are trying to do (more or less well), but I don't really want to write more without full context. – user17732522 Feb 12 '23 at 18:34
  • @ciamej In either case, getting it correct in the sense of well-definedness per standard is tricky and subtle mistakes are very easy to make. – user17732522 Feb 12 '23 at 18:38
  • I'm basically working with this https://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue algorithm – ciamej Feb 12 '23 at 19:36
  • I could always keep T's and atomic counters separately, but that memory layout is not optimal for cache – ciamej Feb 12 '23 at 19:37