NoesisGUI

Coding Style Guide

The purpose of creating a Coding Style Guide is to allow creating code that is easier to understand, review and revise after you write it. It is very important that everyone working in the team reads this document before starting to write code.

Most of the rules enumerated here are arbitrary and their main objective is to create a consistent set of naming conventions to improve the readability across development groups.

3 books are recommended after reading this document (in order of importance):

C++ rules

File format

Use 4 spaces per indentation level. Tabs must be avoided. As code is normally visualized with fixed width font this allows everyone seeing exactly the same layout.

Main blocks inside a file are separated with double blank lines. For the rest, only single blank lines are allowed. For example:

[FILE HEADER]


[GUARDS]


[INCLUDES]


[CODE]

The following header should be used for all the files:

////////////////////////////////////////////////////////////////////////////////////////////////////
// Noesis Engine - http://www.noesisengine.com
// Copyright (c) 2009-2010 Noesis Technologies S.L. All Rights Reserved.
// [CR #586 #1030]
////////////////////////////////////////////////////////////////////////////////////////////////////

When files are reviewed in a code review, they are marked with the CR tags shown in the example above. The ticket associated to that code review is indicated. In the example, the file was reviewed in the codereview described in the ticket 586. Later it was reviewed again in the codereview 1030.

Limit all lines to a maximum of 100 characters. Comment blocks have exactly this width and can be used to check that lines are not beyond the limit. In Visual Studio there is a little trick to show column guides. You should add this entry to your registry:

[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\8.0\Text Editor]
"Guides"="RGB(220, 220, 220) 100"

All expressions introduced by the programmer should be in English. This applies to all kinds of text a programmer might use in source file: type names, variable names, function names, comments and explanations. An implication of this rule is that abbreviations should only be used if they are generally accepted.

Header Files

For include files (.h) the guards should follow the file header. The guard name must be: __MODULE_PACKAGE_H__. For example:

[MemoryManager.h]

////////////////////////////////////////////////////////////////////////////////////////////////////
// Noesis Engine - http://www.noesisengine.com
// Copyright (c) 2009-2010 Noesis Technologies S.L. All Rights Reserved.
////////////////////////////////////////////////////////////////////////////////////////////////////


#ifndef __CORE_MEMORYMANAGER_H__
#define __CORE_MEMORYMANAGER_H__


#endif

The first include in a header file (.h) should be always Noesis.h. The rest of the include files in a header file must be only the strictly needed ones.

Include statements should be sorted and grouped, first by privacity (public or private) and then, for public includes, by Package.

Whenever possible forward class declarations to reduce dependencies and speed up compilation times. Forwarded classes are always grouped in comment blocks. For example:

// Forward declarations
//@{
namespace Core
{
class TypeClass;
class BaseObject;
struct ReflectionFunctions;
template<class ClassT, class BaseT> class TypeClassCreator;
template<class EnumT, class EnumHelperT> class TypeEnumCreator;
}
namespace Render
{
NS_INTERFACE IRenderView;
NS_INTERFACE IRenderTarget;
NS_INTERFACE IVertexBuffer;
NS_INTERFACE IIndexBuffer;
NS_INTERFACE IVertexSource;
}
//@}

Implementation Files

For implementation files (.cpp) the first include should be always its own header file. This guarantees that the used header file is self sufficient for compilation (it doesn't depend on other include file). You should not create include files that rely on one header file to include another one.

The root names of the .cpp and the .h files should match exactly. Several classes may be implemented in a single .h/.cpp unit as long as they form a cohesive unit (for example a class and the iterator for that class). If you have more than one class in a file, identify each class clearly. Give the file a name that is related to the class name. A class name CustomerAccount would have files named CustomerAccount.cpp and CustomerAccount.h, for example.

Include statements should be sorted and grouped as in header files. As the first file to include is always the header file, public or private includes block can come first depending on the privacity of the header file. Includes to external files (from Third Parties) are always the last ones.

Angle brackets are used for public includes, double quotes for local (private) includes. Include file paths must never be absolute. Includes to public headers (inside the Include folder of each package) of packages must always contain the name of the module. For example, to include the public header {{{IFile.h}}} from the Core module, {{{#include <NsCore/IFile>}}} must be written. Includes to private headers (inside the Src folder) of other packages packages are forbidden.

#include "AnsiFile.h"
#include "PrivateFile.h"

#include <NsCore/Boxing.h>
#include <NsCore/Thread.h>
#include <NsFile/IFileSystem.h>

#include <fstream>
#include <math.h>

Macros are allowed after include files. For third party includes, macros could be necessary before the include itself.

#include <NsDrawing/Image.h>
#include <NsDrawing/Color.h>
#include <NsCore/LoggerMacros.h>

#define FREEIMAGE_LIB
#include <FreeImage.h>

#define NS_LOG_GUI(...) NS_LOG(LogSeverity_Info, NST("Gui"), __VA_ARGS__)
#define NS_GUI_ENABLE_PARSER_LOG

NS_DECLARE_SYMBOL(Key)
NS_DECLARE_SYMBOL(Value)
NS_DECLARE_SYMBOL(Name)

Namespaces

At least two level of namespaces are used. The first one is the Noesis namespace. The second one is the namespace for the module. More namespace levels can be used but it is not usual. Namespaces are not indented.

namespace Noesis
{
namespace Core
{

}
}

All the packages should be always under this two-level namespace hierarchy. To avoid excessive typing implementation files are allowed to use using namespace directives whenever it is possible. The using namespace directive is not allowed in public header files to avoid polluting the global namespace.

#include <NsRender/CommandBuffer.h>
#include <NsCore/MemProfiler.h>


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Render;

Anonymous namespaces must be used for local declarations in implementation files.

Comments

Lines starting with three slashes should be used when commenting classes, functions, enums, etc. These comments are used to generate code documentation automatically.

/// The format of the Index Buffer
enum IndexFormat
{
    /// Indices are 16 bits each
    IndexFormat_16,
    /// Indices are 32 bits each
    IndexFormat_32
};

To apply comments to blocks the following syntax is used:

/// Copy constructor and copy operators disabled
//@{
Kernel(const Kernel& copy);
Kernel& operator=(const Kernel& copy);
//@}

Function parameters are documented this way:

/// Allocates a block of memory of specified size
/// \param size Size in bytes of the block allocated
/// \return A pointer to the new allocated block of memory
/// \remarks Thread-safe
virtual void* Alloc(NsSize size) = 0;

Parameter comments exceeding 100 columns must be splitted in lines indented with 4 spaces:

/// Clear items in position
/// \param position Position in the internal array to clear the stored
///     items (if the position is already cleared an exception is raised)
void ClearItems(NsSize position);

By default, functions are not considered Thread-Safe. The remarks section is used to indicate that a function is Thread-Safe.

Before a comment there should be a blank line. After the comment a blank line shouldn't appear. This helps a reader scan the code.

/// Creates a vertex source
virtual Ptr<IVertexSource> CreateVertexSource(const VertexSourceDesc& vertexSourceDesc) = 0;

/// Creates a command buffer
virtual Ptr<CommandBuffer> CreateCommandBuffer() = 0;

Comments describing a class and its members must only appear in the declaration file (.h) and not in the implementation file (.cpp) to avoid desynchronization.

The comments to indicate that a part is not finished or that something has to be revised use the token TODO plus the name of the author.

// TODO: [marlo] Probably this is not the better algorithm
//       [claudio] I've improved the algorithm to O(n log n), but I think it can be done
//  in linear time O(n)

Naming Schemes

Definition of the naming schemes referred to in the rest of the document:

  • Lower mixed case: The name is written in lowercase, each new word begins with a capital. For example: lowerMixedCase, localToWorld.
  • Upper mixed case: Same as lower mixed case except it begins with a capital letter. The first letter after a digit is uppercase. For example: UpperMixedCase, LocalToWorld.
  • Upper underscore: The name is written in uppercase, words are separated by underscores. For example: UPPER_UNDERSCORE_CASE, LOCAL_TO_WORLD

This table describes how the different described schemes are applied to in each case:

  Naming scheme Examples
Type Names Upper mixed case class Book, NsInt32
Typedef Names Upper mixed case typedef Noesis::Core::Int NsInt
Variable Names Lower mixed case Book book, totalObjects
Named Constants Upper mixed case const NsFloat32 Pi = 3.141592f
Function Names Upper mixed case void CalculatePosition()
Macro Names Upper underscore case NS_ASSERT(b < 5)
Namespaces Names Upper mixed case namespace Noesis
Template Type Names Upper mixed case template<class C, class D>

When declaring a function typedef the new type should be suffixed with 'Fn':

typedef void (*RegisterObjectsFn)(IObjectFactory*, NsBool);
RegisterObjectsFn registerObjects =
    reinterpret_cast<RegisterObjectsFn>(GetProcAddress(handle, "RegisterObjects"));

Typedefs, functions and classes outside the Noesis namespace must be prefixed with 'Ns'. For example:

namespace Noesis
{
namespace Core
{
class Symbol;
}
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/// Global access of symbol type.
////////////////////////////////////////////////////////////////////////////////////////////////////
typedef Noesis::Core::Symbol NsSymbol;

Pointers and references should have their reference symbol (* for pointers, & for references) next to the type rather than to the name:

NsInt32* ptr;
const Vec3f& position = obj->GetPosition();

It is recommended that generic variables have the same name as their type. For example:

void OpenFile(File* file)

Abbreviations and acronyms must be uppercase when used as name:

void ExportHTMLSource();  // NOT: ExportHtmlSource
void OpenDVDPlayer();     // NOT: OpenDvdPlayer
class UIElement;          // NOT: UiElement
class VGLSurface;         // NOT: VglSurface

Code Block Layouts

Examples showing the recommended style and layout for each code block:

for (NsInt i = 0; i < count; ++i)
{
    //...
}
if (value > 100)
{
    //...
}
else
{
    //...
}
switch (option)
{
    case Option_Yes:
    {
        //...
        break;
    }
    case Option_No:
    {
        //...
        break;
    }
    case default:
    {
        // default case option must be managed always
        NS_ASSERT(false);
        break;
    }
}

As shown in the examples, in construction like for, switch and if a space is always left before the parenthesis.

Blocks following a control-flow statement (if, switch, etc.) should always be enclosed with braces. This is applied to one-sentence blocks too.

Before and after some operators a space is mandatory. Specifically for comparisons (==, <, >, etc) and assignment operators (=) spaces are needed after and before. Comma and semi colon characters(, ;) only have the space after them. Otherwise spaces can be used along with parentheses to structure and make more readable complicated formulas:

a = 2;

if (b == 1)
{
    a = 1;
}
else if (b == 2)
{
}
else
{
}

for (NsInt i = 0; i < 10; ++i)
{
}

while (true)
{
}

do
{
}
while (true)

result = ((x1 + y1) / (x1 + z1)) + (a1 + b1) / (a1 + c1) * ((x2 + y2) / (x2 + z2)) +
    (a2 + b2) / (a2 + c2);

When a line doesn't fit into the anchor limits (100 characters) the next line must be indented with a single tab.

////////////////////////////////////////////////////////////////////////////////////////////////////
void MyClass::SomeProcess(const NsChar* fileName, const std::vector<NsString> nameList,
    bool processAll, NsInt& count, std::vector<NsString> processedNameList)
{
    HMODULE fileModule = CreateFile(fileName, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL, 0);

    if ((fileModule != INVALID_HANDLE_VALUE) && ((nameList.size() > NS_MIN_NAMES_TO_PROCESS) ||
        processAll))
    {
        /// ...
    }
}

Basic Types

In <NsCore/Types.h> (automatically included by <Noesis.h>) several types are defined that must be used instead of the standard ones to ensure compatibility across different platforms.

  • NsByte, 8-bit unsigned
  • NsInt8, 8-bit signed
  • NsUInt8, 8-bit unsigned
  • NsInt16, 16-bit signed
  • NsUInt16, 16-bit unsigned
  • NsInt32, 32-bit signed
  • NsUInt32, 32-bit unsigned
  • NsInt64, 64-bit signed
  • NsUInt64, 64-bit unsigned
  • NsInt, default integer
  • NsUInt, default unsigned integer
  • NsSize, Anything that represents a size in bytes or an index
  • NsIntPtr, pointers that are treated as integers
  • NsUIntPtr, pointers that are treated as unsigned integers
  • NsPtrDiff
  • NsFloat32, 32-bit IEEE floating point.
  • NsFloat16, 64-bit IEEE floating point.
  • NsBool, boolean
  • NsChar8, single byte character
  • NsChar16, two byte-wide character
  • NsChar, redirected to NsChar8 or NsChar16 depending of compilation settings
  • NsString8, single byte string
  • NsString16,two byte-wide string
  • NsString, redirected to NsString8 or NsString16 depending of compilation settings

NsInt should be used whenever the size of the integer is not important. NsInt is the most efficient integer for the architecture (in a 32 bit OS it is an alias for NsInt32).

Do not assume that sizeof(NsChar) == sizeof(NsInt8), because in Unicode compilations that is not true. Strings should always be written enclosed with the macro NST.

const NSChar msg[] = NST("This is a string");

Classes

The layout of a class has the following parts:

  1. Header comment describing the class
  2. Constructors and destructors
  3. Public functions: class routines first, inherited routines next
  4. Protected functions
  5. Protected member variables
  6. Private functions
  7. Private member variables

Member variables should start with m. That way it is easy to distinguish class variables from local scratch variables. Static member variables should start with s.

Every class member variable should be declared one at each line.

Between the type and the instance name only one space is allowed. Spaces to align member variables are not allowed.

Before the public, private and protected keywords a blank line should appear (or a line with a { ). After the keyword the blank line shouldn't appear. Functions and member variables are considered different blocks and must separately be enclose inside a public, protected, private block. See example below.

The implementation of the class in the cpp file must follow the same function order specified in the include file. The only exception are static variables that should appear before any function implementation and constructors and destructors that should be always the first functions in the cpp file.

Methods to get or set a data member of a class are called accessor methods. Names of accessor methods are written in upper mixed case and use the name of the accessed class member, prefixed by 'Get' or 'Set'.

When a class implements an Interface, the functions belonging to that interface and enclosed with a //@{ ... //@} block and the comment From IInterface is used. The same is used when reimplementing functions inherited from a base class. See the example below.

[MyClass.h]

////////////////////////////////////////////////////////////////////////////////////////////////////
// Noesis Engine - http://www.noesisengine.com
// Copyright (c) 2009-2010 Noesis Technologies S.L. All Rights Reserved.
////////////////////////////////////////////////////////////////////////////////////////////////////


#ifndef __TEST_MYCLASS_H__
#define __TEST_MYCLASS_H__


#include <Noesis.h>


namespace Noesis
{
namespace Test
{

////////////////////////////////////////////////////////////////////////////////////////////////////
/// MyClass. Here a brief description of the class.
/// For long descriptions a link to documentation (wiki) pages is prefered.
////////////////////////////////////////////////////////////////////////////////////////////////////
class MyClass: public MyBase, public IMyInterface
{
public:
    /// Default constructor
    MyClass();

    /// Copy constructor
    MyClass(const MyClass& myClass);

    /// Destructor
    ~MyClass();

    /// Get the current position
    /// \return The current position
    const Vec3f& GetPosition() const;

    /// Set a new position
    /// \param position The new position
    void SetPosition(const Vec3f& position);

    /// From IMyInterface
    //@{
    void Init();
    void Shutdown();
    //@}

private:
    /// Check the internal state of the instance
    void CheckState();

private:
    Vec3f mPosition;
    NsInt32 mCount;
};

}
}


#endif
[MyClass.cpp]

////////////////////////////////////////////////////////////////////////////////////////////////////
// Noesis Engine - http://www.noesisengine.com
// Copyright (c) 2009-2010 Noesis Technologies S.L. All Rights Reserved.
////////////////////////////////////////////////////////////////////////////////////////////////////


#include "MyClass.h"


using namespace Noesis;
using namespace Noesis::Test;


////////////////////////////////////////////////////////////////////////////////////////////////////
MyClass::MyClass(): MyBase(), mPosition(0.0f, 0.0f, 0.0f), mCount(0)
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
MyClass::MyClass(const MyClass& myClass): MyBase(), mPosition(myClass.mPosition),
    mCount(myClass.mCount)
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
MyClass::~MyClass()
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
const Vec3f& MyClass::GetPosition() const
{
    return mPosition;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void MyClass::SetPosition(const Vec3f& position)
{
    mPosition = position;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void MyClass::Init()
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void MyClass::Shutdown()
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void MyClass::CheckState()
{
}

To improve interface readability, function declaration and implementation is always separated. For inline functions, an inl file is used for the inline implementation. This file is included at the end of the header file. The .inl file is always included outside namespaces. Namespaces must be declared again in the .inl file. Include files that are only needed by the .inl file must me moved from the .h file to the .inl

[ObjectPool.h]

////////////////////////////////////////////////////////////////////////////////////////////////////
// Noesis Engine - http://www.noesisengine.com
// Copyright (c) 2009-2010 Noesis Technologies S.L. All Rights Reserved.
////////////////////////////////////////////////////////////////////////////////////////////////////


#ifndef __CORE_OBJECTPOOL_H__
#define __CORE_OBJECTPOOL_H__


#include <Noesis.h>
#include <NsCore/KernelApi.h>


namespace Noesis
{
namespace Core
{

////////////////////////////////////////////////////////////////////////////////////////////////////
/// A pool of objects. The template parameter T indicates the type of object to be create in each
/// allocation. The template parameter N indicates the number of object in each internal chunk
/// (a contiguous block of memory)
////////////////////////////////////////////////////////////////////////////////////////////////////
template<class T, NsInt N> class ObjectPool
{
public:
    /// Constrcutor
    ObjectPool();

    /// \return An object allocation
    T* Allocate();

    /// Free a pointer
    /// \param A pointer previously allocated with Allocate()
    void Deallocate(T* obj);

private:
    FixedAllocator fixedAllocator;
};

}
}

// Inline include
#include <Core/ObjectPool.inl>

#endif
[ObjectPool.inl]

////////////////////////////////////////////////////////////////////////////////////////////////////
// Noesis Engine - http://www.noesisengine.com
// Copyright (c) 2009 Noesis Technologies S.L. All Rights Reserved.
////////////////////////////////////////////////////////////////////////////////////////////////////


namespace Noesis
{
namespace Core
{

////////////////////////////////////////////////////////////////////////////////////////////////////
template<class T, NsInt N>
ObjectPool<T, N>::ObjectPool()
{
    fixedAllocator.Initialize(sizeof(T), sizeof(T) * N);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
template<class T, NsInt N>
T* ObjectPool<T, N>::Allocate()
{
    void* ptr = fixedAllocator.Allocate();
    return new(ptr) T;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
template<class T, NsInt N>
void ObjectPool<T, N>::Deallocate(T* obj)
{
    obj->~T();
    fixedAllocator.Deallocate(obj);
}

}
}

By default all functions must be declared no inline (with the exceptions of templates, of course, that must be always inlined). Later, in an optimization phase, the studied functions that are giving problems may me moved to inline. The main idea of this rule is to avoid prematurely incrementing the size of the executable and compilation times.

Structs

Although in the C language, class and struct is conceptually the same. In Noesis, we use struct for small classes with all the members public and without functions. The m should not be added to variable names of the struct. Constructors and destructor are usually defined within structs.

struct Data
{
    NsFloat32 a;
    NsFloat32 b;
    NsFloat32 c;
};

Interfaces

Interfaces follow the same rules that classes but all the functions should be pure virtuals and variable members are not allowed. Instead of the class keyword, NS_INTERFACE is used.

[IMemoryStream.h]

////////////////////////////////////////////////////////////////////////////////////////////////////
// Noesis Engine - http://www.noesisengine.com
// Copyright (c) 2009 Noesis Technologies S.L. All Rights Reserved.
////////////////////////////////////////////////////////////////////////////////////////////////////


#ifndef __CORE_IMEMORYSTREAM_H__
#define __CORE_IMEMORYSTREAM_H__


#include <Noesis.h>
#include <NsCore/ICommon.h>
#include <NsCore/Reflection.h>


namespace Noesis
{
namespace Core
{

////////////////////////////////////////////////////////////////////////////////////////////////////
/// IMemoryStream. Interface for a memory block
////////////////////////////////////////////////////////////////////////////////////////////////////
NS_INTERFACE IMemoryStream: public ICommon
{
    /// \param buffer Returns the pointer to the internal buffer
    /// \param buffer Returns the size of the internal buffer
    virtual void GetBuffer(void*& buffer, NsSize& size) = 0;

    /// \param buffer The new internal buffer. Previous one is released
    /// \param size The size of the buffer
    virtual void SetBuffer(void* buffer, NsSize size) = 0;

    NS_IMPLEMENT_INLINE_REFLECTION_(IMemoryStream, ICommon)
};


}
}


#endif

Enums

Enumeration values are written in upper underscore beginning with the name of the enumeration followed by an underscore:

enum TextAlignment
{
    TextAlignment_Left,
    TextAlignment_Right,
    TextAlignment_Center
};

The declaration of enumerations is usually outside the class that uses them. For example:

/// RenderTarget Type
enum RenderTargetType
{
    RenderTargetType_Texture,
    RenderTargetType_ShadowMap
};

////////////////////////////////////////////////////////////////////////////////////////////////////
/// IRenderSystem
////////////////////////////////////////////////////////////////////////////////////////////////////
NS_INTERFACE IRenderSystem: public Core::ICommon
{
    /// Creates a RenderTarget
    virtual Ptr<IRenderTarget> CreateRenderTarget(NsSize width, NsSize height,
        RenderTargetType type) = 0;
};

This is the preferred option. This way you can write

renderSystem->CreateRenderTarget(256, 256, RenderTargetType_ShadowMap);

instead of the more verbose

renderSystem->CreateRenderTarget(256, 256, IRenderSystem::RenderTargetType_ShadowMap);

Platform-dependent blocks

Blocks that are enclosed by #ifdef / #endif preprocessor directives (normally platform specific code) must be hierarchically nested. For example:

#ifdef _WIN32
    #include <windows.h>
#else
    #include <pthread.h>
#endif

This rule is not followed inside classes or functions. In this case, the source code must be written as if the preprocessor directives didn't exist. The preprocessor directives must be aligned to the left. For example:

////////////////////////////////////////////////////////////////////////////////////////////////////
void TLSValue::Free()
{
    NS_ASSERT(IsInitialized());

#ifdef _WIN32
    if (TlsFree(mKey) == 0)
    {
        HandleWinError(NST("TlsFree"));
    }
#else
    int status = pthread_key_delete(mKey);
    if (status != 0)
    {
        HandleSystemError(NST("pthread_key_delete"), status);
    }
#endif

    mKey = 0;
}

Python rules

© 2017 Noesis Technologies