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?
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