Nebula 2 Coding Guidelines
Here are summarized main conventions to have in mind when writing code for Nebula 2 engine. The goal is to write code reliable, readable and with good performance. Most of them are common sense rules and conventions to make the source code looks similar independently on the programmer who wrote it, so in general follow the style of existing code. Don't use your own C++ idiom, use the Nebula 2 idiom.Not all of Nebula currently follows these guidelines, but we're slowly and steadily moving into compliance. Use it at least for new code, and revise old code as you go through it for maintainment, additions, etc.
- Indentation
- Syntax
- Naming Rules
- Code portability
- Code conventions
- Documentation
- C++ Header files
- C++ Source files
Indentation
- Indentation. Set your editor to use 4 spaces for each level of indentation and use tabs as spaces. Never use tabs characters in your code.
- Follow the examples below to correctly indent these statements:
if (0 == i) { i = j; } else if (i < 0) { i++; } else { i--; }
for (i = 0;i < 10;i++)
{
a[i]++;
}
do { i++; } while(i < 10);
while (i < 10)
{
i++;
}
switch (i) { case 1: break; case 2: break; default: break; }
Syntax
- Use the carriage return consistently depending on the platform you are working on. Use CR LF for Windows, and LF for Unix. The version control system will work well if this is done, never mix them or you will be in trouble.
- Braces. Always use both braces { } for if, while and for statements. Even if there is only one statements. Check examples in Indentation.
- Use spaces to clarify expression, between operators, assignments, comparison, function arguments, etc. But use exactly one space, not more.
a = b;
(a <= b)
(a == b)
a = b + 2 * exp;
- Use parenthesis to group operations to make clearer the exact order of evaluation, specially for less common operators. This way everyone can understand without having to know the whole table of rules for operator precedence.
- Variable declaration. Normally define each variable in a different line, specially unrelated variables, variables you have to comment (so you can write the comment just above it), and pointer type variables.
/// pointer to the beginning of the name
char * ptrbegin;
/// pointer to the end of the name
char * ptrend;
/// rectangle coordinates
int x0, y0, x1, y1;
- Write code that don't need to be commented if possible, by choosing meaningful names for variables and avoiding complex and innecesary flow controls. Write clear code, it is easier to maintain. Write comments before each function/method declaration and definition (see examples above).
- Avoid writing comments after class members or variables. Comments written before and in separate lines are preferred, and easier to maintain and read.
Naming Rules
- All Nebula class names start with a lowercase 'n', followed by the name of the class camel-cased (CamelCase). Example: nRoot, nObject, nKernelServer.
- Variables in general (objects, arguments, member variables) start with lowercacse always and are camel-cased too. Examples: i, indexCount, resourceName.
- Method member names start with capital letter and are camel-cased. Examples: Open, Close, OpenFile.
- Do not use underscores in any name for classes, variables or functions.
- Do not use prefixes like m_ for function members.
- Do not use hungarian notation. The only prefixes allowed in N2 are:
- ref for references.
- Use your common sense when naming variables: make them speak for themselves. Avoid short names or abbreviations. When naming bool variables, make their meaning obvious. Avoid negated names (eg notDone).
- Loop variables can have a short name if you use them only at the beginning of the loop.
- Standard naming for files is:
- nclassname.h: header file for class nClassName.
- nclassname.cc: source file for class nClassName, in case nClassName is not derived from nobject or component.
- nclassname_main.cc: source file for class nClassName, when it is nobject or component derived. Of course if this gets too long you can split it several, naming them as you consider the best, but following the established patterns, e.g. nclassname_server.cc, nclassname_device.cc would be good names.
- nclassname_cmds.cc: source file typically used for the scripting, it has the n_initcmds, the C scripting wrapper functions, etc.
- Always use lowercase letters for filenames.
Code conventions
- Do not use magic numbers in code. Define a symbol for them, and use it. E.g., if you need to use a char array for a NOH path, declare it like: char str[N_MAXPATH]; instead of using hardcoded max sizes. Check all macros and type definitions at ntypes.h and use them whenever possible.
- Make your code const-correct. Put const for parameters, return values, member methods always that is required, for example for getters, etc. This helps the compiler to generate more optimal code.
- Use 'const char *' when you want functions/methods to take input string arguments or to return constant strings allocated in an object. It is more generic and efficient. Use nString when you need to return something back or when you have to modify an inout argument, and it is not worth to complicate the caller with memory management issues.
- Always reference instance variables with 'this->', so it's easy to distinguish from locals. Don't use globals unless strictly necessary.
- In general, don't make functions that return allocated memory / objects, that must be deallocated explictly by the caller. This is a common source of memory leaks. Make the caller to allocate them if possible.
- Methods returning objects are allowed provided they are not worth for inlining. Modern compilers optimize return value optimization (RVO or NRVO). Example, imagine an object returning a string, you can define it to return it by value or be passed by reference. The first way is clearer and easier to use, and it is optimized correctly by the compiler. Problem here is with MSVC 7.1 and 8.0 (not checked other compilers), which do RVO, but when applied it prevents any inlining optimization (even if forced manually). Normally methods returning objects have to build the object, which is normally costly, in this case the inline optimization is not so important.
/// method 1 nString GetFileName(); /// method 2 void GetFileName(nString & filename);
- Always declare the function body for constructors and destructors at the beginning of the _main file of the class, even if it is empty (put a comment in this case). The same holds true for virtual methods defined in abstract classes.
nRoot::nRoot()
{
// empty
}
- Do not commit code with warnings, the goal is not just removing them to have a cleaner output, the real goal is to discover and fix errors. Do not just remove the warning to avoid seeing it, first take a look and think why it happens. Typical errors involved uninitialized variables, not used variables, etc. Enable maximum level of warnings for all your modules.
- Always initialize all the C++ member variables in the constructor.
- Use asserts wisely. Asserts are placed to find programmer errors, not user errors. So normally, they must never appear to a user using a software distribution. For cases where the user can force it somehow, then you must write proper error control code. Additionally, code poping up asserts must never be comitted. Following macros for asserts (check ndebug.h):
- n_assert(exp). Only execute expression if asserts enabled.
- n_verify(exp). Execute expression always. Only popup if asserts enabled.
- n_assert_always(). Assert always if reached.
- n_dxtrace(hr,msg). Assert hr return code is OK.
- n_dxverify(exp). Assert exp returns code OK. Always executes, only popup if asserts enabled.
- n_assert2(exp, msg). Same as n_assert with message.
- n_verify2(exp, msg). Same as n_verify with message.
- n_assert2_always(msg). Same as n_assert2 with message.
- n_dxverify2(exp,msg). Same as n_dxverify with message.
- Always check input parameters to a function/method with asserts or error control depending on the case. E.g. check an input parameter is not a NULL pointer.
- Always check return status when a function is called, either with assets or error control depending on the case. This is valid either for Nebula 2 functions or functions from external libraries. E.g. check if the opening a file was succesful, calls to Direct X, etc.
- In general don't use n_printf, use log system (NLOG) which can be disabled and configured.
- Do not overuse the usage of inline methods in headers. Normally, modern compilers do this automatically better than we do. And having code just in source files has many advantages when we have to compile, less dependencies, shorter compile times. The only things allowed for inlining is short code that is called many times (e.g. inside loops), but it is better to wait until we have profiling data to determine if we should inline a function or not.
- Avoid generation of temporary objects. Temporary objects are usually harmless, and in some cases are unavoidable. However, when you deal with complex objects whose construction and destruction are expensive in performance terms, you should minimize their introduction. This is more important in loops and functions called often. Temporaries are created in the evaluation of complex expressions, the compiler has to store intermediary results in a temporary location. Tips about temporaries:
- Break complex expressions into autonomous subexpressions, and store their results in a named object. If you need to store multiple intermediary results sequentially, you can reuse the same object. This is more efficient than letting the complier introduce a new temporary in every subexpression.
- Another useful technique is to use += for self-assignment instead of +. Remember that the expression temp + s3 yields another temporary object; by using += you avoid this.
- Don't pass objects greater than 32 bits by value. Instead consider passing them as const reference or pointer instead, which is much more optimal.
Documentation
- Document your module(s) with a separate doxygen file. At least half a page with some functional analysis, some terminology, class diagram with some (brief) description for each, feature lists, things like that. This is for the high level documentation.
- Lower level goes with the source code, as commented in other sections. Don't repeat anything that's already in the code: that's what automatic documentation is for.
- Try to document code as you write it. Even before writing it, by writing some pseudocode that can be used as documentation.
- Write programmer documentation in doxygen format, not in the wiki, in this way we have all the documentation in one place and cross-linked correctly. Wiki is mainly for things that have no code yet, thinkings, ideas. As soon as you implement it, move what is in wiki to the the doxygen.
- Final user documentation is written in wiki format currently, so everybody (not only programers) can contribute to that.
- Use
- Todo:
- doxygen keyword for temporal code, not-finished code, quick and dirty hacks. This can be seen later all in a doxygen page.
C++ Header files
- Standard header for the header file nclass.h:
#ifndef N_CLASS_H
#define N_CLASS_H
//------------------------------------------------------------------------------
/**
@class nClass
@ingroup NebulaObjectSystem
Description of the purpose of the class.
(C) 2005 Copyright holder
*/
//------------------------------------------------------------------------------
#include "nhashnode.h"
//------------------------------------------------------------------------------
class nObject;
//------------------------------------------------------------------------------
class nClass : public nHashNode
{
public:
/// constructor
nClass();
/// destructor
~nClass();
private:
nClass * parent;
};
//------------------------------------------------------------------------------
/**
More detailed comment.
*/
inline
nClass:xxx()
{
}
//------------------------------------------------------------------------------
#endif //N_FILENAME_H
- Only put include headers strictly necessary to make this file compile when it is included from other files, remember you must use forward declarations whenever it is possible. This minimizes dependencies and compilation time.
- Function and method declarations. Functions and methods are declared in one line. If that is too long for a single line, break by one of the arguments and use one level of indentation. Always put one line doxygen (///) comment before the function declaration.
/// Sets the value of this object to the passed integer
void SetI(int i);
/// remove all bindings matching the given signal, object, cmdproto
bool UnbindSignal(nFourCC signal4cc, const nObject * object,
const nCmdProto * cmdProto);
- Never include precompiled headers in header files.
- Do not put any code inside the C++ class declaration, this way we can see clearly the specification of a class without code in between.
- Document methods and variables briefly in the header file (just one-line comments), and include a more extense documentation in the implementation.
- Keep header files (.h files) small, with minimal or no code at all, not even at the end of the class declaration. In this way, we avoid large recompilation on code changes, this is very important for large projects. Modern compilers like MSVC do this automatically much better than we can do it manually.
- Only one class per file, allowing exceptions just for local classes.
C++ Source files
- The standard format for C++ files.
//-----------------------------------------------------------------------------
// nclass.cc
// (C) 2005 Copyright holder
//-----------------------------------------------------------------------------
#include "precompiled/pchnkernel.h"
#include "nclass.h"
//-----------------------------------------------------------------------------
/**
method doc
*/
nClass::~nClass()
{
// empty
}
//-----------------------------------------------------------------------------
- Function and method definition. Start with the standard Nebula 2 80-char hyphen separation line. Next place a multiline doxygen comment which specifies what the function does and what is each parameter and return type. If you want to specify some internal implementation detail for the whole function of how are done things, do it but separated from the comments of its usage. The return type must be on a different line (inline qualifier in a different line too). Then the rest of the function declaration, and then the code. Check following example. Place one empty line between the end of the function and the beginning of the separation line for the next.
//------------------------------------------------------------------------------
/**
sets the file pointer to given absolute or relative position
@param byteOffset the offset in bytes
@param origin position from which to count in bytes
@return success
internal documentation.
*/
inline
bool
nFile::Seek(int byteOffset, nSeekType origin)
{
// assert input parameters
// method code
// return whatever
}
Code portability
Just some simple common sense comments about code portability:
- Use int/float as normal type when you need numbers. Do not use nint8/16/32/64 unless required, e.g. to access a memory buffer.
- Introduce all platform specific code protected, either in separate modules or with #ifdef PLATFORM depending on the most convenient for each case.