Moving to C++ From C

Started by
11 comments, last by Ectara 11 years, 4 months ago

In C++, the equivalent is "placement new", which is how you manually call a constructor on a block of memory (whereas regular new allocates memory and calls the constructor). However, you've got to be very careful doing this kind of stuff, if you want to keep regular C++ behaviour -- destructors won't be called when the memory goes out of scope, so when using placement-new, you must also manually call the destructors of your objects at the appropriate times.

Just like old times. :)


Instead of overriding the new operator with my own version, I've chosen to invent my own keyword via a macro - eiNew (ei is my engine's "macro prefix", short for "eight", the name of the engine). If I use any 3rd party code that relies on new, then it behaves as usual.

That's a good idea. My allocator didn't override malloc() at link time like many would, so I suppose I'm familiar with that coexistence.


My eiNew macro uses a stack allocator to grab enough memory for the new object, then uses placement new to construct the object in that area. It also adds the address of the object to a linked-list belonging to a "scope" object (which is a RAII-type object), which is used to call the destructor when the scope is destructed.

An object that maintains the scope of the variables, and calls their destructors for them? Very nice. :)


But for cases where I want to use memory other than the built-in call stack, my eiNew macro mimics this regular behaviour for my own buffers:

I see that the stack pointer simply increments whenever you need memory. My allocator allows for private arenas and memory pools that can be created in a function call and make a stack allocator, but I'm sure that it's nowhere near as fast as a simple linear allocator.


Most of the time when I use malloc, it's to grab big buffers, like "buffer" above, and then I use eiNew for everything.

Reading through, I assume that it means that you only ever allocate objects in order, and the lifetime of your objects follow a strict schedule. Do you ever have a need to allocate an object whose lifetime is not decided by the place where it is allocated?
Advertisement
An object that maintains the scope of the variables, and calls their destructors for them? Very nice.
This is the whole point of RAII, a key idiom in C++. I wish I could take credit for this "[font=courier new,courier,monospace]Scope[/font]" class, which associates multiple objects' lifetimes with a single "RAII object", but I have to credit DICE for that idea wink.png
It's kind of similar to an array of [font=courier new,courier,monospace]std::unique_ptr[/font]'s, I guess.
Reading through, I assume that it means that you only ever allocate objects in order, and the lifetime of your objects follow a strict schedule. Do you ever have a need to allocate an object whose lifetime is not decided by the place where it is allocated?[/quote]When I first read through DICE's presentation, I did think this would be a bigger issue (because I was used to writing "typical" C++ at the time), but now I've built an engine from the ground up using these kinds of allocators, and the majority of the time it has worked out!
For cases where it's not suitable, I'll often allocate a fixed size pool (using [font=courier new,courier,monospace]eiNew[/font]) and then objects can be allocated within the pool with varying lifetimes (as long as none of the objects need to live longer than the pool itself).

Sometimes lifetimes are too hard to reason about, in which case, reference counting via [font=courier new,courier,monospace]std::shared_ptr[/font] can be a good solution, however, I find these cases to be quite rare.
My engine has interoperability with a Lua VM, which has a garbage collector. Usually if I'm writing code with unpredictable lifetime rules, then it happens to be Lua code, and I don't have to worry about it wink.png

Also, sometimes simply using [font=courier new,courier,monospace]malloc[/font]/[font=courier new,courier,monospace]free[/font] can be the simplest solution, and KISS is a good principle! When I use [font=courier new,courier,monospace]malloc[/font] in C++, I try to use it via a RAII wrapper, which automatically calls [font=courier new,courier,monospace]free[/font] for me.
Going off on a tangent -- the NonCopyable class in my code that I've linked to, is a feature I picked up from boost (much of which is now in [font=courier new,courier,monospace]std::[/font]). It's a neat trick that stops people from accidentally using the assignment-operator or copy-constructor on classes that shouldn't be cloneable.
e.g. If I was to make my [font=courier new,courier,monospace]ScopedMalloc[/font] class compliant with the rule of three, then it would need an assignment operator and a copy-constructor, which logically would use [font=courier new,courier,monospace]memcpy[/font] to clone the allocation... However, this doesn't seem like a feature I'd ever want to use, so instead I inherit from [font=courier new,courier,monospace]NonCopyable[/font], so if anyone every tries to clone a [font=courier new,courier,monospace]ScopedMalloc[/font], then they'll get a compile error instead. If they actually want to clone their allocation, they can write that manually (and more importantly, I'm still complying with the rule-of-three now).

This is the whole point of RAII, a key idiom in C++. I wish I could take credit for this "Scope" class, which associates multiple objects' lifetimes with a single "RAII object", but I have to credit DICE for that idea Posted Image
It's kind of similar to an array of std::unique_ptr's, I guess.

This sounds pretty idyllic; I'm used to calling destructors and cleanup code EVERYWHERE, including all places that the function returns.


When I first read through DICE's presentation, I did think this would be a bigger issue (because I was used to writing "typical" C++ at the time), but now I've built an engine from the ground up using these kinds of allocators, and the majority of the time it has worked out!
For cases where it's not suitable, I'll often allocate a fixed size pool (using eiNew) and then objects can be allocated within the pool with varying lifetimes (as long as none of the objects need to live longer than the pool itself).

I have an allocator for this very purpose, but stack allocation is much more simple than mine, which is basically an optimized malloc(); I may invest in such an allocation scheme. I'm heavy into using the stack for allocations for everything that won't cause a stack overflow, won't be of varying size, and won't need its lifetime to be longer than the scope where it was allocated.


Sometimes lifetimes are too hard to reason about, in which case, reference counting via std::shared_ptr can be a good solution, however, I find these cases to be quite rare.
My engine has interoperability with a Lua VM, which has a garbage collector. Usually if I'm writing code with unpredictable lifetime rules, then it happens to be Lua code, and I don't have to worry about it Posted Image

Reference counting is very effective, but I hesitate to use it a lot of times; if two reference counted objects refer to each other, then go out of scope at the same time, an implementation might choke, resulting in both of them being unreclaimable since there is still a reference to both of them.


Also, sometimes simply using malloc/free can be the simplest solution, and KISS is a good principle! When I use malloc in C++, I try to use it via a RAII wrapper, which automatically calls free for me.

Even that is more handy than just doing it the old fashioned way.


Going off on a tangent -- the NonCopyable class in my code that I've linked to, is a feature I picked up from boost (much of which is now in std::). It's a neat trick that stops people from accidentally using the assignment-operator or copy-constructor on classes that shouldn't be cloneable.
e.g. If I was to make my ScopedMalloc class compliant with the rule of three, then it would need an assignment operator and a copy-constructor, which logically would use memcpy to clone the allocation... However, this doesn't seem like a feature I'd ever want to use, so instead I inherit from NonCopyable, so if anyone every tries to clone a ScopedMalloc, then they'll get a compile error instead. If they actually want to clone their allocation, they can write that manually (and more importantly, I'm still complying with the rule-of-three now).

That's one of the reasons that I like inheritance; it allows more than just specifying what it should have, but also indirectly what it shouldn't have.

This topic is closed to new replies.

Advertisement