Re: Calling destructor on fundamental types and other stuff about
placement new
Gennaro Prota <gennaro.prota@yahoo.com>, on 27/08/2010 07:42:53, wrote:
On 27/08/2010 1.43, Francesco S. Carta wrote:
Hi there,
in order to get a better grip on the stuff about "operator new", "new
operator", "placement new" and so forth I went back to the relevant
sections of TC++PL and of the FAQ, then I've implemented a simple Vector
template trying to follow the implementation of std::vector that I found
in my implementation (ehm... OK, you know what I mean despite that
repetition).
[...]
At some point I tested it with a fundamental type:
Vector<int> vec(10);
....and the template instantiation went fine, the program compiled and
ran as expected.
But then I thought: wait, I am calling the equivalent of "ptr->~int()"
within my destroy(), but if I write it directly, say something like this:
int* pi = new int;
pi->~int();
....then the compiler - as I expected - rejects it.
My conclusion is that the template mechanism is able to understand that
I'm going to do something useless with fundamental types and simply
ignores the "ptr->~value_type()" line when instantiating the template
with "value_type == some fundamental type".
Well, no, not the template mechanism. It's more like Johannes
said :-)
Note the note (!) in [class.dtor]/15, too (who said the C++
standard doesn't have a rationale; it just "inline" :-))
I see now, I shall force myself to refer to the standard before asking
such kind of questions here :-(
Any comment or further insight on the above?
Sooner or later I'll post here the complete implementation of my Vector
to get some advice (and some corrections, very likely), but in the mean
time I'm most concerned with the basic storage management internals that
I'm pasting here below, so please have a look and point out any wrong or
silly thing I could be doing:
I had a quick look. The abundance of reinterpret_casts is what
jumped most to my eye. (Well, duh... they have been invented to
stand out. I meant that you don't need them.)
value_type* allocate(size_t n) {
return
reinterpret_cast<value_type*> (
new char[sizeof(value_type) * n]
);
}
I think the best option for this function, if you want to have
it, is to just return a void *, which is in general the way to
warn/inform the reader that he is dealing with (yet) "untyped"
memory:
I forgot to make it explicit that those functions are private members of
my Vector template - I also forgot to make them static, but I'll fix it,
although it shouldn't change much. The purpose of this exercise is to
create a Vector as self-contained as possible.
By centralizing the conversion from void* to value_type* I can simply write:
start = allocate(n);
....in the implementation of other methods such as reserve(), where
"start" is a private value_type* data member of Vector, pointing to the
beginning of the allocated space.
void *
allocate( size_t n )
{
return new char[ n * sizeof... ] ; // or even just
// operator new( n * sizeof...)
// which I would prefer.
}
And I'd use unsigned char, although in C++ char probably works
too (I have never dug into whether it really does. IIRC char may
have trap representations in C. And --this is the part I've not
dug into-- it perhaps cannot have them in C++. Since unsigned
char is the de facto standard to signal "raw bytes", I just use
unsigned char and go on. In fact, the last time I tried to dig,
I seemed to find several oddities in the standard, so why
risking.)
I'm not so sure I really need to switch to unsigned char, the standard
makes explicit examples with "raw" char for this technique, I think it's
required to work in all cases.
Back to your exercise, I'd recommend to mentally separate the
places where you can assume that there is an object constructed
in the buffer from the ones where you just have raw memory. And
code the details from there, with some private functions being
in fact the "transitions" between these two kinds of states.
I'll be back on this in a while.
void deallocate(void* ptr) {
delete[] reinterpret_cast<char*>(ptr);
}
I'd call the operator delete[] function. No need to
reinterpret_cast.
Uh... actually, before adding that cast, I wrote:
delete[] ptr;
....and the compiler warned about "deleting void* is undefined"... it did
not cross my mind that I could explicitly call operator delete[] as you
suggest (it didn't just because I didn't know that!).
Now my code reads:
operator delete[] (ptr);
....in that line. I wonder why the compiler doesn't resolve "delete[]
ptr" to "operator delete[] (ptr)" and get rid of the warning... I need
more study to understand the issue, any further insight will be more
than welcome.
value_type* create(void* ptr) {
return reinterpret_cast<value_type*>(new(ptr) value_type());
}
This is a placement new *expression*: you already get a
value_type * (so this is another reinterpret_cast that goes
away). And do you need to return anything from the function?
value_type* create(void* ptr, const value_type& t) {
return reinterpret_cast<value_type*>(new(ptr) value_type(t));
}
Likewise.
Eh, of course I don't need to cast them... silly me... but thanks for
pointing it out.
About the return value, I used it for a check at the calling place,
something like this:
assert(ptr == create(ptr));
....because at some point I thought that the new expression could
actually mangle the address to align it properly... now I almost sure
that such a thing could never happen.
void destroy(value_type* ptr) {
ptr->~value_type();
}
For your first experiments it is probably easier to use a
non-heap array, with one object. Just something like a
unsigned char m_buffer[ sizeof( T ) ]
member in your class template (which, given that it contains at
most one object, you'd probably no longer call "vector" :-)).
That would get rid of allocate() and deallocate() and perhaps
even show more clearly that you are not doing any "placement
delete" (see [lib.new.delete.placement]). Then, for create() and
destroy() I think I'd just have:
void construct_object( T const& ) ;
void destroy_object() ;
And for their implementation, off the top of my head:
void *
address() // NOTE: depending on what you do, you might need a
// const version of this, too
{
return m_buffer ;
}
template< typename T>
void
...::construct_object( T const& t ) // PRE: first call, or first call
{ // after destroy_object
new ( address() ) T( t ) ;
}
template< typename T> // PRE: an object exists (its lifetime
void // has not ended)
...::destroy_object()
{
T * p( static_cast< T *>( address() ) ) ;
p->T::~T() ;
}
Note the separation I was talking about: before entering
construct_object you have to assume that there's no object in
the buffer (call it twice in a row and you have a problem :-)).
At its return though, you can assume that *there is* an object
(leaving the function with an exception doesn't count as a
"return", of course). destroy_object() does the opposite
transition: it must be entered when there is an object (and so
you can static_cast) and at its return you have raw memory.
Note, too, that if address() returned a char *, you couldn't
static_cast it to T * directly.
The Vector implementation is already advanced enough to need dynamic
management of the storage, as I already have working reserve(),
push_back() and clear(), now I'm working on insert() and erase(), but I
think I will stop when I'll reach begin() and end() without implementing
reverse_iterator - more about this (and about its rationale) in a
further post, where I'll show my complete implementation for the public
delight && dissection - assuming short-circuit behavior at that "logical
and" ;-)
Thanks a lot for your notes Gennaro.
--
FSC - http://userscripts.org/scripts/show/59948
http://fscode.altervista.org - http://sardinias.com