Re: Safe reuse of allocated storage
On Feb 5, 2:06 pm, Nikolay Ivchenkov <ts...@mail.ru> wrote:
Consider the following example:
#include <memory>
struct X
{
X(int &r) : ref(r) {}
int &ref;
};
int m;
int n;
int main()
{
std::allocator<X> a;
X *p = a.allocate(1);
a.construct(p, m);
p->ref = 1; // well-defined
a.destroy(p);
a.construct(p, n);
p->ref = 1; // leads to undefined behavior
a.destroy(p);
a.deallocate(p, 1);
}
This program sequentially creates two objects of type X on the same
memory location. The object of type X created first I will call "the
first object" and the object of type X created second I will call "the
second object".
According to N3225 - 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 storage for the new object exactly overlays the storage location
which the original object occupied, and
- the new object is of the same type as the original object (ignoring
the top-level cv-qualifiers), and
- 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
- the original object was a most derived object (1.8) of type T and
the new object is a most derived object of type T (that is, they are
not base class subobjects).
------------------------------------------
According to N3225 - 3.9.2/3:
------------------------------------------
If an object of type T is located at an address A, a pointer of type
cv T* whose value is the address A is said to point to that object,
regardless of how the value was obtained.
------------------------------------------
Thus, p cannot be used to access the member ref of the second object
(probably, a compiler is allowed to assume that in both cases p->ref
refers to the same object, though that's not so). Presumably, the
following approach should not imply undefined behavior:
#include <memory>
struct X
{
X(int &r) : ref(r) {}
int &ref;
};
int m;
int n;
int main()
{
std::allocator<X> a;
void *pv = a.allocate(1);
X *p1 = static_cast<X *>(pv);
a.construct(p1, m);
p1->ref = 1;
a.destroy(p1);
X *p2 = static_cast<X *>(pv);
a.construct(p2, n);
p2->ref = 1; // well-defined now?
a.destroy(p2);
a.deallocate(p2, 1);
}
Here formally pv can never point to the first object, because its type
is not "cv1 pointer to cv2 X". Pointer p2 never points to that object
too. But what may happen if we use single pointer p (as shown below)
instead of p1 and p2?
#include <memory>
struct X
{
X(int &r) : ref(r) {}
int &ref;
};
int m;
int n;
int main()
{
std::allocator<X> a;
void *pv = a.allocate(1);
X *p = static_cast<X *>(pv);
a.construct(p, m);
p->ref = 1;
a.destroy(p);
p = static_cast<X *>(pv); // reassignment
a.construct(p, n);
p->ref = 1; // well-defined?
a.destroy(p);
a.deallocate(p, 1);
}
Does the reassignment with the same pointer value help to avoid
undefined behavior in this case?
My guess, is that the intent of the restriction on const members is
due to optimization a compiler might take. Maybe in some cases the
compiler can avoid the initialization of const members while
constructing the object, which may involve using a different layout of
the class.
References are just like const pointers.
If that's the reason, then keeping the pointer value in another
variable and reassigning wouldn't help. You'll need to get the value
in the pointer that is passed to the later placement-new command that
created the later object.
However, if that had been the reason, I would expect that section to
require it recursively.
"...does not contain any non-static data member whose type is const-
qualified or a reference type"
I think should have added: and neither any of its non-static data
members or base class recursively.
It doesn't state that recursively for whatever reason. So formally if
you wrapped you class X in a wrapper, and allocate Wrapper<X> instead:
template< typename T >
class Wrapper
{
//data
private: T m; //non const member
//method
public: T* get() { return &(this->m); }
//forwarding constructors...
template< ... > Wrapper( ... ) { ... }
};
Then you are allowed to use the old Wrapper<X>* with it's get() method
(because "can be used to manipulate the new object").
* However, keeping the Foo* pointer returned by a previous call to
get() will become invalid by the wording of the standard.
If you can use some kind of smart pointer that keeps the Wrapper<X>*
and converts to Foo* by calling get() you could work-around that.
Although, it requires trusting that the wording of the standard
intentionally don't mention non-const recursively.
template< typename T >
Pointer
{
//data
private: Wrapper<T>* m;
//xtor
public: Pointer( Wrapper<T>* r )
:
m(r)
{}
//methods
T* operator->() const
{
return m->get();
}
};
int main()
{
std::allocator<Wrapper<X>> a;
Pointer<X> p = a.allocate(1);
a.construct(p, m);
p->ref = 1; // well-defined
a.destroy(p);
a.construct(p, n);
p->ref = 1; // now this is well-defined per the current wording
a.destroy(p);
a.deallocate(p, 1);
}
itaj
--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]