Tutorial 71: Memory Management in C++
C++ vs C# memory model
XNA games are written in C#, which has a garbage collector. When a Texture2D object goes out of scope in C#, the GC eventually reclaims the CPU-side managed memory, and the finalizer releases the underlying GPU resource. This can lead to GC pauses but the programmer rarely needs to think about it.
CNA games are written in C++, which has no garbage collector. Every GPU resource (Texture2D, VertexBuffer, Effect, RenderTarget2D) that you allocate must be released explicitly, or it leaks both the CPU backing allocation and the underlying GPU resource (texture object, VBO, FBO). The C++ idiom for deterministic cleanup is RAII.
RAII in CNA
RAII (Resource Acquisition Is Initialisation) ties resource lifetime to object lifetime. A Texture2D object's constructor acquires the GPU resource; its destructor releases it. As long as the Texture2D is held in a smart pointer (std::unique_ptr or std::shared_ptr), the resource is released when the smart pointer goes out of scope or is reset.
CNA's classes implement an IDisposable-style interface via a Dispose() virtual method, mirroring XNA's pattern. You can call Dispose() explicitly to release a resource early (e.g. when unloading a level), or let the destructor call it automatically.
The Dispose(bool) override pattern
CNA base classes call a protected Dispose(bool disposing) virtual method. Override it in your subclass to release your own resources:
class MyGame : public Game {
protected:
// Called when the game is shutting down, or when Dispose() is called.
// disposing == true : called from Dispose() — safe to release managed objects.
// disposing == false : called from destructor — only release unmanaged resources.
void Dispose(bool disposing) override {
if (disposing) {
// Release CNA managed objects in reverse LoadContent order
spriteBatch_.reset();
texture_.reset();
effect_.reset();
}
// Call parent implementation
Game::Dispose(disposing);
}
};
std::unique_ptr vs raw pointers
Always prefer std::unique_ptr for game objects and GPU resources. Never use new/delete directly in game code.
| Pattern | Recommendation |
|---|---|
std::unique_ptr<T> | Single owner. Default choice for assets, game objects, and GPU resources. |
std::shared_ptr<T> | Shared ownership. Use when multiple systems hold a reference (e.g. a texture used by many sprites in different systems). |
Raw pointer T* | Non-owning reference only. Use for passing a resource to a function that does not claim ownership. |
new / delete | Avoid. If you must use them, immediately wrap in a smart pointer. |
Proper RAII resource ownership in a game class
#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
class MyGame final : public Game {
public:
MyGame() : graphics_(this) {
graphics_.setPreferredBackBufferWidth(1280);
graphics_.setPreferredBackBufferHeight(720);
}
// Non-copyable, non-movable — Game owns the GraphicsDevice
MyGame(const MyGame&) = delete;
MyGame& operator=(const MyGame&) = delete;
protected:
void LoadContent() override {
auto& gd = getGraphicsDeviceProperty();
// All GPU resources created here and stored in unique_ptr members.
// They are released in reverse order when the unique_ptrs are destroyed.
spriteBatch_ = std::make_unique<SpriteBatch>(gd);
texture_ = std::make_unique<Texture2D>("assets/hero.png", gd);
effect_ = std::make_unique<BasicEffect>(gd);
// Procedurally built vertex data owned by a local vector,
// then uploaded and the local vector discarded.
std::vector<VertexPositionColor> verts = BuildTriangle();
vb_ = std::make_unique<VertexBuffer>(
gd, VertexPositionColor::VertexDeclaration,
static_cast<int>(verts.size()), BufferUsage::None);
vb_->SetData(verts.data(), static_cast<int>(verts.size()));
// verts is released here — GPU copy is all we need.
}
void UnloadContent() override {
// Called by the framework before the GraphicsDevice is destroyed.
// Reset smart pointers in the opposite order of creation.
vb_.reset();
effect_.reset();
texture_.reset();
spriteBatch_.reset();
}
void Update(const GameTime&) override { /* game logic */ }
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::CornflowerBlue);
spriteBatch_->Begin();
spriteBatch_->Draw(*texture_, Vector2(100.0f, 80.0f), Color::White);
spriteBatch_->End();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> texture_;
std::unique_ptr<BasicEffect> effect_;
std::unique_ptr<VertexBuffer> vb_;
};
Move semantics
CNA objects support move construction and move assignment (they delete copy operations). Moves transfer ownership without copying the GPU resource:
// Wrong: Texture2D is not copyable
Texture2D a("logo.png", gd);
Texture2D b = a; // compile error: copy constructor is deleted
// Correct: move transfers ownership; 'a' becomes empty
Texture2D b = std::move(a);
// 'a' is now in a valid-but-unspecified state; do not use it.
// With unique_ptr (preferred):
auto tex1 = std::make_unique<Texture2D>("logo.png", gd);
auto tex2 = std::move(tex1);
// tex1 is now nullptr; tex2 owns the GPU texture.
Common pitfalls
Double-free
Calling Dispose() twice on a CNA object is safe — subsequent calls are no-ops. However, deleting a raw pointer twice is undefined behaviour:
// WRONG — double delete, undefined behaviour
Texture2D* t = new Texture2D("img.png", gd);
delete t;
delete t; // crash or heap corruption
// Correct — unique_ptr prevents this
auto t = std::make_unique<Texture2D>("img.png", gd);
// t is automatically deleted once, when it goes out of scope.
Dangling references
Storing a raw pointer or reference to a CNA object and then resetting its owning unique_ptr leaves a dangling pointer:
// WRONG
SpriteBatch* rawPtr = spriteBatch_.get();
spriteBatch_.reset(); // SpriteBatch is destroyed
rawPtr->Begin(); // dangling pointer — undefined behaviour
// Correct: only keep raw pointers for the duration of a function call
void DrawSprites(SpriteBatch& sb) {
sb.Begin();
// ... use sb
sb.End();
// sb is a reference, valid as long as the caller's unique_ptr is alive
}
Accessing released GPU resources
If you call Dispose() on a Texture2D and then try to bind it to a shader uniform, the underlying OpenGL or Vulkan handle is invalid. Always destroy resources after their last use, not before.
LoadContent / UnloadContent patterns
The CNA framework calls UnloadContent before the GraphicsDevice is torn down. This guarantees that GPU resources are released while the device is still valid. Do not release GPU resources in your destructor if there is any chance the device has already been destroyed — use UnloadContent instead.
For level streaming, call ContentManager::Unload() to release all assets loaded through that manager, then load the new level's assets. Use separate ContentManager instances per level to make unloading clean:
// In your Game subclass
std::unique_ptr<ContentManager> levelContent_;
void LoadLevel(const std::string& levelName) {
// Unload previous level assets
if (levelContent_) levelContent_->Unload();
// Create a fresh ContentManager for this level
levelContent_ = std::make_unique<ContentManager>(
Services, "Content/" + levelName);
// Load assets for the new level
playerTexture_ = levelContent_->Load<Texture2D>("player");
backgroundTex_ = levelContent_->Load<Texture2D>("background");
}