Classes & Structures

Preface

Design your objects to be as simple and straightforward as possible, and achieve one single goal as best as possible. If your junior software developer friend can't make sense of your high-level code, give these articles a read:

Declarations

Class/structure declarations are done on one line, with the body starting on the next line. Only use one space after the class and struct specifier.

struct ComposedObject
{
    int x, y;
};
template<typename Type>
class Thing : public Type
{
};

New Class/Struct/Tool Files

Use standard #ifndef/#define/#endif include guards.

Do not use #pragma once as not all compilers support this feature.

Inherited Class Formatting

  • Format your classes' and structures' inherited objects in one column.
  • Sort the inherited objects from broadest to tightest scope.
  • Sort the classes in the scope alphabetically.
  • Always respecify the scope.
  • Only use one space before and after the colon (':') which is separating the class declaration and the base-list.
  • Always place the comma after the class / struct name, against the name.
    • This is to keep the code along the lines of regular English grammar.
class Foo : public Bar,
            public SomethingUseful,
            protected OtherBar,
            private Rebar
{
};

Class/Struct Layout Rules

Layout

  • Unless there is only a need for the default scope which all variables and methods fall under, always specify the scope modifier.
  • But never respecify the same scope modifier.
  • Order the scope modifiers by most open, down to the most closed.
  • Only specify scope modifiers once - never respecify class/struct scope.
    • If there are custom constructors, either:
    • Specify the default constructor.
  • Hide the default constructor in the private section of the class/struct.
  • For every scope:
    • Always specify constructors first, in this order:
      • Default constructor first.
      • Custom constructors next.
      • Copy-constructor afterwards.
      • Move-constructor (if applicable).
      • Destructor.
      • Always specify custom operator after constructors.
      • Always specify custom methods afterwards.

Destructors

Only specify the destructor if:

  • The class is intended to be derived from.
    • You must prefix it with virtual in this case.
  • The class has a particular destruction sequence you need to adhere to.
  • The class needs to kept within a container of some kind.

Copyability

Always disallow copyability, unless the class design is entirely POD - which is more rare than you think.

If the design is not going to account for copyability or is going to disallow it, place the copy constructor and operator= (and move editions where applicable) at the bottom most part of the private section of a class or struct.

Be sure to not fill in these methods - not even for private uses! That would be a confusing design!

If you can, mark the functions with = delete. JUCE's JUCE_DECLARE_NON_COPYABLE and JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR do this for you already using JUCE_DELETED_FUNCTION.

If copyability is an expected behaviour, then manually fill in all of the copying methods: copy constructor and operator= (and move editions where applicable). Doing all of them guarantees consistent logic, avoiding default generated code from a potentially poor compiler. It's a bit more effort on your end, but at least you're ready to fix it - and static analysers are available to help prevent mistakes!

For more information, see the Rule of Three/Five.

class IAmCopyable
{
public:
    IAmCopyable() :
        bar (0)
    {
    }

    IAmCopyable (const IAmCopyable& other) :
        bar (other.bar)
    {
    }

    IAmCopyable& operator= (const IAmCopyable& other)
    {
        if (this != &other)
            bar = other.bar;


        return *this;
    }

private:
    int bar;
};
class IAmNotCopyable
{
public:
    IAmNotCopyable() :
        bar (0)
    {
    }

private:
    int bar;

    JUCE_DECLARE_NON_COPYABLE (IAmNotCopyable)
};

Warning

Whatever you do, don't hurt your designs by creating a base-class to designate copyability or cloneability! Ask yourself what it means to make your object copyable, and design high-level classes to generate non-copyable objects.

Good

Bad

Avoid Using the this Pointer

This is only for the rare cases where you're passing the class instance down into other classes and functions.

The reasoning is to make obvious the segregation between class/struct/function dependencies.

Initialisation in a Member List

Use = for primitives and nullptr associations, and { } for anything else.

class Example
{
public:
    Example()
    {
    }

private:
    int bar = 0;
    std::atomic<bool> foo { false };

    JUCE_DECLARE_NON_COPYABLE (Example)
};