General C++ Tips

This tutorial features some tips for GD modding and C++ programming in general.

Use auto

Despite what some GD modders might tell you, using auto is perfectly fine and actually usually desirable. auto is the C++-equivalent of the let keyword in languages like JavaScript or var in C#; it makes C++ infer the type of a variable. For example:

// inferred as int
auto x = 5;
// inferred as std::string
auto str = functionThatReturnsString();
// inferred as float
auto z = 5.2f;

However, do note that auto can lead to unexpected behaviour due to implicit type conversions. For example:

// str is an std::string
std::string str = "Awesome string!";
// Whoops! str is a const char*
auto str = "Awesome string!";

This code can not be converted to auto by just replacing the type, as this actually calls std::string’s const char* constructor. The type of string literals is const char*, so if you want to create an std::string from a string literal, it’s usually best to avoid using auto. Of course, you could do this:

auto str = std::string("Awesome string!");

But that’s more verbose than the original code you started with, and it’s not really more readable.

Avoid C-style casts

C++ has inherited a lot of technical debt from C, and one of these is C-style casts:

float x = 5.3f;
int y = (int)x;

You should avoid using them for nearly all situations. This is because C-style casts are wholly unpredictable. For example, in the above code, what do you thinks happens? Does the float get rounded down to 5? Does the float’s binary representation gets reinterpreted as an integer?

C++ provides more explicit alternatives that make it clear what you want:

float x = 5.3f;
int y = static_cast<int>(x);

The main ones are static_cast, reinterpret_cast, and dynamic_cast. There are also some others like const_cast, some casting functions in the standard library like std::static_pointer_cast and std::duration_cast, and some very magical and terrifying aliases in Geode like reference_cast and as, but in general these will only be used in specific situations. For most cases, you should just use static_cast.

⚠️ A lot of older mods use reinterpret_cast for a lot of things, but this is not recommended at all. You should always use static_cast over reinterpret_cast, unless you are very sure about what you are doing.

static_cast is what you would expect type conversion to do: it converts the value to match the requested type. This works for casting between built-in datatypes, downcasting pointers, casting between your own classes etc..

dynamic_cast is a safe version of static_cast for polymorphic classes. For example:

class A {};
class B : A {};

if (auto b = dynamic_cast<B*>(a)) {
    // b is certainly a valid instance of B
}

In this code, b will either be certainly a valid B or nullptr. dynamic_cast comes with a runtime cost though, so if you know that b will definitely be a valid B, you can use static_cast instead.

⚠️ For GD mods, you should not use dynamic_cast on Cocos2d nodes. This is because, due to problems, dynamic_cast<ButtonSprite*>(spr) will always return null regardless of what you’re expecting. Instead, Geode provides an alternative: typeinfo_cast.

Avoid memory leaks

C++ does not have a garbage collector or any sort of memory management, so you have to be careful with handling your own memory. In general, the following tips will help:

Always allocate on the stack when possible

struct Thing {
    float x;
    float y;
};

class OtherThing {
    Thing m_thing; // value
};

void someFun(Thing const& thing) {} // pass by const reference

If you’re passing small structs or classes, always allocate it on the stack by just writing its name. This is the easiest way to keep track of memory; the one built-in memory management C++ does have is RAII, which means that once your struct is no longer used, its memory will be freed. Allocating on the stack is also generally much faster than allocating on the heap.

Allocating objects on the stack also makes dealing with exceptions and early returns easier; no need to add delete everywhere, C++ will automatically free the object for you.

{
    Thing x;
    // no matter what happens, be it exceptions or 
    // something else, x will be freed at the end
}

Use references over pointers

If you’re passing data to a function or a shared parent class to a child, use references instead of pointers. References are safer for a lot of reasons, and also don’t increment memory. However, when passing by reference, you have to make sure your class outlives the reference - for example, the following code will crash:

struct SomeThing {
    int& m_num;
};

SomeThing* thing;
{
    int x = 5;
    thing = new SomeThing { x };
}
thing->m_num; // Whoops! x is out of scope and so what 
              // m_num is referencing has been deallocated

Be aware of lifetimes

In C++, objects created on the stack live until they go out of scope, and objects created on the heap using new live until you call delete on them. References and pointers to the object can not extend this lifetime, so make sure that if you’re referencing an object, it has not been freed.

For example, some libraries may require you to pass pointers to objects, but you probably want to just pass pointers to stack-allocated objects (since they are automatically freed, so you don’t have to worry about calling delete)

int x;
someLegacyFun(&x); // make sure from someLegacyFun's docs that it only uses the 
                   // pointer to x inside its body, and doesn't store it anywhere

Use CCObject-based class over normal ones

Cocos2d comes with its own handy garbage collector, and if you create something like a CCArray, its memory will be automatically freed when it’s no longer used. Be wary though that this means you have to make sure any CCArray you do actually need doesn’t get garbage-collected. Cocos2d can’t know if you’ve assigned the CCArray to a variable or class member, so either use the Ref class in Geode to ensure the object stays alive, or manually keep track of it using retain if you have to. (Ref should be used in nearly all situations though)

If you’re making your own class that inherits from CCObject (for example, a node), always make sure to call autorelease on it. This will ensure the garbage collector will clean its memory. (Unless you are working with TableViewCells. In that case, never call autorelease unless you want to experience the worst bug of all time.)

Use smart pointers over pointers

If you really do have to use pointers and the class you’re working with isn’t CCObject-based, prefer using smart pointers like shared_ptr and unique_ptr over raw pointers. Smart pointers automatically manage the memory being pointed to, and free it when it’s no longer needed.

For every call to new you have, there must exist a matching call to delete

If none of the other strategies are applicable and you have to use raw pointers, the rule of thumb for memory management there is for every call to new you have, there must be a matching call to delete. The only exception to this are global manager classes, which are never freed in lieu of being static. Also, in Cocos2d code, you will call new for all of your own node classes, but never delete; this is because Cocos2d calls delete for you when the garbage collector frees the object.