C++ Logo

sg12

Advanced search

[ub] Implementation of assignment in std::optional and Core issue 1404: Object reallocation in unions

From: Nikolay Ivchenkov <mk.ivchenkov_at_[hidden]>
Date: Thu, 30 May 2013 15:49:47 +0400
The following concerns are related to the suggested implementation of
std::optional - see
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3672.html
https://github.com/akrzemi1/Optional/blob/master/optional.hpp

Consider the following example:

    #include "optional.hpp"
    #include <iostream>

    struct A
    {
        constexpr A(int &x) : ref(x) {}
        int &ref;
    };

    int main()
    {
        int n1 = 0, n2 = 0;
        std::experimental::optional<A> opt = A(n1);
        opt.emplace(n2);
        opt->ref = 1;
        std::cout << n1 << " " << n2 << std::endl;
    }

Here initialization of variable opt implies initialization of union member
storage_.value_ (which has type A). Then expression
opt.emplace(n2)destroys object
storage_.value_ via explicit destructor call and creates new object by
placement form of new-expression (using forwarded n2 in the
new-initializer). All public functions that provide access to the stored
value (operator->(), operator *(), value(), etc.), obtain pointer/reference
through object expression storage_.value_.

This is a simplified version of the above code:

    #include <iostream>

    #define FORWARD(x) static_cast<decltype(x) &&>(x)

    template <class T>
        union U
    {
        U(T &&x) : value_(x) {}
        unsigned char dummy_;
        T value_;
    };

    template <class T>
        struct optional
    {
        constexpr optional(T &&x) : storage_(FORWARD(x)) {}
        template <class... Params>
            void emplace(Params &&... params)
        {
            storage_.value_.~T();
            new (&storage_.value_) T(FORWARD(params)...);
        }

        U<T> storage_;
    };

    struct A
    {
        constexpr A(int &x) : ref(x) {}
        int &ref;
    };

    int main()
    {
        int n1 = 0, n2 = 0;
        optional<A> opt2 = A(n1);
        opt2.emplace(n2);
        opt2.storage_.value_.ref = 1;
        std::cout << n1 << " " << n2 << std::endl;
    }

The question is: What may happen at line

    opt->ref = 1;

in the former code or

    opt2.storage_.value_.ref = 1;

in the latter (simplified) code?

According to N3485 - 3.8/7,

    If, after the lifetime of an object has ended and before the
    storage which the object occupied is reused or released, a new
    object is created at the storage location which the original
    object occupied, a pointer that pointed to the original object, a
    reference that referred to the original object, or the name of the
    original object will automatically refer to the new object and,
    once the lifetime of the new object has started, can be used to
    manipulate the new object, if:

    [...]
    — the type of the original object is not const-qualified, and, if
      a class type, does not contain any non-static data member whose
      type is const-qualified or a reference type, and
    [...]

In our case the cited condition is not satisfied, because A has a
non-static data member of a reference type — ref.

It looks like a compiler is free to assume that opt->ref or
opt2.storage_.value_.ref still refers to n1 (as if the accessed ref would
be member of the old object) rather than n2 (to which new ref is supposed
to refer). Is this interpretation correct? What else may happen? If the
aforementioned implementation of std::optional is unreliable, how would you
suggest to improve it?

Received on 2013-05-30 13:49:48