3

I have seen the following pattern several times:

// T is a type, this is at namespace scope
std::aligned_storage_t<sizeof(T), alignof(T)> storage;
T &t = reinterpret_cast<T &>(storage);

This, coupled with adequate namespacing and naming, provides a pleasant interface (t) to users of the variable, while enabling deferred construction, reinitialization, etc of the actual object on the library side though placement new and explicit destructor calls. You can see it working here.

Now, std::aligned_storage is neat and all, but C++17 gave us a new tool in the box for such storage-vs-object lifetime splitting, that is std::optional.

However, the two ways of accessing the value of an std::optional (value() and operator*) both require a value to actually be there; otherwise value() will throw std::bad_optional_access, while operator* will trigger undefined behaviour (via breaking the requires clause in [optional.observe]§5).

std::optional<T> storage;
T &t = *storage; // Looks okay, mines bitcoin when you're not looking

Is such a usage of std::optional still possible somehow?
If not, what would be a reason for preventing it?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Quentin
  • 62,093
  • 7
  • 131
  • 191
  • 1
    Doesn't using `t` safely in the first example requires running a bunch of bookkeeping related checks beforehand? – StoryTeller - Unslander Monica Dec 26 '17 at 12:09
  • @StoryTeller it does require a bit of caution, but nothing that a dash of SBRM can't handle. Often the object is actually scoped to `main`, and it's just about avoiding doing too much stuff during static initialization/destruction while keeping the global access point. – Quentin Dec 26 '17 at 12:12
  • 1
    I'm pretty sure `T &t = reinterpret_cast(storage);` exhibits undefined behavior - or rather, the subsequent use of `t` would. The right way to do it is `T& t = *new(&storage) T;`. That's essentially equivalent to setting the value in `std::optional`. So in the end, it's six of one, half a dozen of the other. – Igor Tandetnik Dec 26 '17 at 15:49
  • @IgorTandetnik this should be equivalent to the pointer juggling [there](https://stackoverflow.com/q/13466556/3233393), which apparently is fine. – Quentin Dec 26 '17 at 15:54
  • From that question: *"I use placement new to create the object"* – Igor Tandetnik Dec 26 '17 at 15:59
  • @IgorTandetnik well, so do I? – Quentin Dec 26 '17 at 16:01
  • Do you? It doesn't appear anywhere in the question, as far as I can tell. My point is, if you do that, then you could at the same spot assign a value to your `optional`, and then `operator*` would work. – Igor Tandetnik Dec 26 '17 at 16:13
  • @IgorTandetnik whoops, it was in the demo code but I forgot to add it to the question itself. That's fixed. But here the deal is to retrieve the pointer *before* the object gets constructed there. – Quentin Dec 26 '17 at 16:15

1 Answers1

3

This, coupled with adequate namespacing and naming, provides a pleasant interface (t) to users of the variable, while enabling deferred construction, reinitialization, etc of the actual object on the library side.

Unfortunately, using t to access the object constructed later at that address is undefined behavior. This is one of the reasons why std::launder is proposed.

Note this case is different from the case described in that question. In that question, the reference/pointer is obtained after the object of type T is created (though this may also be undefined after C++17 without std::launder).

Is such an usage of std::optional still possible somehow?

As what you pointed out, this is undefined behavior.

If not, what would be a reason for preventing it?

An optimizer may find the address is associated with the object that provides storage for T, and ignore any access to that address through a glvalue of type that causes undefined behavior. In fact, the reasons are essentially how strict-aliasing rules benefit an optimizer.

xskxzr
  • 12,442
  • 12
  • 37
  • 77