As you mentioned, this question can really be thought of as going far beyond images in scratch space. I've actually come across this in many different forms (memory sections, typed arrays, threads, network connections, ...).
So what I ended up doing was to write myself a generic "BufferPool". It's a class which manages any form of buffer object, be it a byte-array, some other piece of memory, or (in your case) an allocated image. I loaned this idea from the ThreadPool.
It's a fairly simple class that maintains a pool of Buffer objects from which you can acquire a buffer when you need one and release it back to the pool when you're done with it. The acquire function will check whether there is a buffer in the pool available, and if not, will create a fresh one. If there is one in the pool, it will reset the Buffer, i.e. clear it so that behaves identical to a freshly created one.
I then have a couple of static instances of this BufferPool, one for each different type of Buffer I use: One for byte arrays, one for char arrays, ... (I'm coding in Java, in case you are wondering... :) I then use these static instances in all of the library functions I'm writing. This allows me that, for example, my cryptography functions can share byte-arrays with my binary flattening functions, or any other code in my application. This way, I get maximal reuse of these objects and it has given me a major performance increase in many cases.
In C++ you might be able to implement this use-everywhere scheme very elegantly by writing a custom allocator for the data structures you need based on this pooling technique (Thanks to Andrew for pointing this out; see comments).
One thing I did for my byte-array buffer is that the acquire function will accept a minimumLength parameter that specifies the minimum size of the buffer I need. It will then only return a byte array of at least this length from the pool, or create a new one, if the pool is empty or only has smaller images in it. You could use the same approach with your image buffer. Let the acquire function accept a minWidth and minHeight parameter and then return an image of at least these dimensions from the pool, or create one with exactly these dimensions. You could then have the reset function only clear the (0, 0) to (minWidth, minHeight) section of the image, if you even need it cleared at all.
The one feature that I decided to not worry about in my code, but you might want to consider depending on how long your application will run for and how many different image sizes it will process is whether you want to limit the buffer size in some way and free up cached images to reduce memory use of your application.
Just as an example, here is the code I use for my ByteArrayPool:
public class ByteArrayPool {
private static final Map<Integer, Stack<byte[]>> POOL = new HashMap<Integer, Stack<byte[]>>();
/**
* Returns a <code>byte[]</code> of the given length from the pool after clearing
* it with 0's, if one is available. Otherwise returns a fresh <code>byte[]</code>
* of the given length.
*
* @param length the length of the <code>byte[]</code>
* @return a fresh or zero'd buffer object
*/
public static byte[] acquire(int length) {
Stack<byte[]> stack = POOL.get(length);
if (stack==null) {
if (CompileFlags.DEBUG) System.out.println("Creating new byte[] pool of lenth "+length);
return new byte[length];
}
if (stack.empty()) return new byte[length];
byte[] result = stack.pop();
Arrays.fill(result, (byte) 0);
return result;
}
/**
* Returns a <code>byte[]</code> of the given length from the pool after optionally clearing
* it with 0's, if one is available. Otherwise returns a fresh <code>byte[]</code>
* of the given length.<br/>
* <br/>
* If the initialized state of the needed <code>byte[]</code> is irrelevant, calling this
* method with <code>zero</code> set to <code>false</code> leads to the best performance.
*
* @param length the length of the <code>byte[]</code>
* @param zero T - initialize a reused array to 0
* @return a fresh or optionally zero'd buffer object
*/
public static byte[] acquire(int length, boolean zero) {
Stack<byte[]> stack = POOL.get(length);
if (stack==null) {
if (CompileFlags.DEBUG) System.out.println("Creating new byte[] pool of lenth "+length);
return new byte[length];
}
if (stack.empty()) return new byte[length];
byte[] result = stack.pop();
if (zero) Arrays.fill(result, (byte) 0);
return result;
}
/**
* Returns a <code>byte[]</code> of the given length from the pool after setting all
* of its entries to the given <code>initializationValue</code>, if one is available.
* Otherwise returns a fresh <code>byte[]</code> of the given length, which is also
* initialized to the given <code>initializationValue</code>.<br/>
* <br/>
* For performance reasons, do not use this method with <code>initializationValue</code>
* set to <code>0</code>. Use <code>acquire(<i>length</i>)</code> instead.
*
* @param length the length of the <code>byte[]</code>
* @param initializationValue the
* @return a fresh or zero'd buffer object
*/
public static byte[] acquire(int length, byte initializationValue) {
Stack<byte[]> stack = POOL.get(length);
if (stack==null) {
if (CompileFlags.DEBUG) System.out.println("Creating new byte[] pool of lenth "+length);
byte[] result = new byte[length];
Arrays.fill(result, initializationValue);
return result;
}
if (stack.empty()) {
byte[] result = new byte[length];
Arrays.fill(result, initializationValue);
return result;
}
byte[] result = stack.pop();
Arrays.fill(result, initializationValue);
return result;
}
/**
* Puts the given <code>byte[]</code> back into the <code>ByteArrayPool</code>
* for future reuse.
*
* @param buffer the <code>byte[]</code> to return to the pool
*/
public static byte[] release(byte[] buffer) {
Stack<byte[]> stack = POOL.get(buffer.length);
if (stack==null) {
stack = new Stack<byte[]>();
POOL.put(buffer.length, stack);
}
stack.push(buffer);
return buffer;
}
}
And then, in the rest of all my code where I need byte[]'s, I use something like:
byte[] buffer = ByteArrayPool.acquire(65536, false);
try {
// Do something requiring a byte[] of length 65536 or longer
} finally {
ByteArrayPool.release(buffer);
}
Note, how I added 3 different acquire functions that allow me to specify how "clean" I need the buffer to be that I am requesting. If I'm overwriting all of it anyways, for example, there is no need to waste time on zeroing it first.