Wartbed:Code style

From Dark Omen Wiki

Revision as of 11:45, 28 November 2009 by Mikademus (Talk | contribs)
Jump to: navigation, search
Note: This page isn't something most people are interested in. I am writing and maintaining it more as a guide to myself to help me with coding consistency. Some parts of WARTBED consists of code predating this Standard and may thus deviate from it. If parties interested in assisting with WARTBED feel dissuaded for aesthetics reasons due to this Standard then remember the golden rule: "Ignore all rules to achieve the best possible result".

Also see the WARTBED design document.


The WARTBED coding style Standard is based on the Allman Style, with some significant adaptations. Fundamentally, through bracket placement, blank lines and special comments, the WARTBED code style is aimed at emphasising pedagogics and logical cohesion of code structures over frugality of vertical screen space (unlike f.i. the K&R ["The One True Style"] style), and to promote efficient code through basic calling convention and pointer usage patterns, and the code Standard itself emphasises abstraction layers, functional division and MVC architecture.


C++ Language Facilities

// Use a smart pointer class to store dynamically allocated objects
typedef auto_ptr<Model> PTR_MODEL;
typedef auto_ptr<Renderable> PTR_RENDERABLE;
 
 
void GoodConventions( std::string const &rParam1, DataStruct const &rData )
{
    // Variables are declared where they are used
    PTR_MODEL pShip = ModelFactory::createModel( "Spaceship" );
    PTR_RENDERABLE pHero = SkinModel( pShip, "WingCommander" );
 
    PTR_MODEL pAlien = ModelFactory::createModel( "Alien" );
    PTR_RENDERABLE pEnemy = SkinModel( pAlien, "Kilrathi" );
 
    if (pHero && pEnemy)
    { 
        for (unsigned i = 0; i < 10; ++i)
        {
            BattleStats stats = static_cast<BattleStats &>( DoBattle(pHero, pEnemy) );
            PresentStatistics( stats );
            logger << stats;
        }
    }
}
 
 
// BAD: param is called by-value, not by-reference (const reference)
// BAD: pData is sent as a pointer, requiring checks for NULL
void PoorConventions( std::string param, DataStruct *pData )
{
    BattleStats stats; // BAD: Declared outside where it is used
 
    // BAD: Storing dynamically allocated object in raw pointer
    Model *pModel = new Model( "BFG9000" );
 
    PTR_MODEL pShip = ModelFactory::createModel("Spaceship");
    PTR_MODEL pAlien = ModelFactory::createModel("Alien");
 
    // BAD: If this fails then Alien was created unnecessarily
    PTR_RENDERABLE pHero = SkinModel( pShip, "WingCommander" ); 
    PTR_RENDERABLE pEnemy = SkinModel( pAlien, "Kilrathi" );
 
    if (pHero && pEnemy)
    { 
        // BAD: Trusting the compiler to optimise the post-incrementor
        for (unsigned i = 0; i < 10; i++) 
        {
            // BAD: Using C-style cast where C++'s is more suitable
            stats = *(BattleStats *) &DoBattle( pHero, pEnemy );
            PresentStatistics( stats );
            logger << stats;
        }
    }
}

</div>

Calling conventions

  • Call-by-reference is preferred over call-by-value
  • References are preferred over pointers
  • Const is preferred over non-const
    • The "const" keyword is placed after the data type ("Class const &rC = myC;")


Variables

  • Unless where warranted for batching or special purposes, variables are declared in close proximity to where they are used. This is to ease locating the declaration of symbols, emphasise code flow, align code for MRU stack access, avoid allocation and assignment overhead (f.i. due to pre-emptive code termination), and promote automatic scoping.
    Background: Back in the days, for compiler parsing reasons, some languages (f.i. early versions of C and Pascal) required you to declare all variables immediately following the function signature or the principal open token. This practice still survives among some programmers and is commonly rationalised as "making it easier to find symbols by collating them in one place".
  • Local variables, including loop control variables, should generally be declared in their code structure (i.e. "for (int i = 0; i < 5; ++i)").
  • Explicitly initialise atomic variables, since they are not default-initialised by the compiler
    • Set numerics to a sensible value ("int count = 0;")
    • Initialise pointers to 0 (prefer 0 over NULL) ("int *pInt = 0;")
  • To allow objects to be default-initialised, where suitable provide a default constructor setting the object's state to workable default values


Operators

  • Always prefer pre-increment and pre-decrement operators to their postfix alternatives.
  • C++ casts are preferred over C-style casts.
  • Operators are padded with one space (see horizontal indentation and padding, below).


Pointers

Contrary to some vocal opinions, pointers are not evil or bad. Pointers can be enormously powerful and efficient: thus, with the significant exception for dynamic allocation of memory, do use pointers wherever warranted, if judiciously, with restraint and with discipline.

  • Never use raw pointers by themselves to store dynamically allocated memory. All dynamically allocated objects should be immediately stored inside smart pointer containers to avoid automated resource management and deallocation.
    Bad: "MyObject *pObject = new MyObject;"
    Good: "shared_ptr<MyObject> pObject = shared_ptr<MyObject>( new MyObject );"
    • Corollary 1: Never return new's pointers as a result from a function, instead always return smart pointers.
    • Corollary 2: This also means passing a new'd pointer in function call parameter lists, as well as any other place where the allocated resource isn't immediately stored in a automated container is prohibited.

Other C++ Language Facilities

C++ is a feature-rich language. These features make it an expert-friendly but sometimes beginner-hostile language. The feature set of C++ makes it a multi-paradigm language that is well-suited to represent problems of many domains and allows the programmer to chose the solution (how to address the problem), unlike other languages where the form of the solution is always already made. This comes at the cost of requiring skill and discipline of the programmer.

  • Operator overloading: Operator overloading is extremely well-suited to address many specific OO-requirements in games development. Mathematics and quasi-mathematical notations is well expressed using this form. However, operator overloading should only be used where it naturally expresses the intention or function of the code, and when it contributes to code pedagogics.
  • Multiple Inheritance: Game programming is one of a few development domains that truly benefits from MI. Just as single inheritance, it should be used judiciously.
  • UNICODE isn't naturally well represented in C++. Nonetheless, wide chars and strings are preferred for all IO actions and textual contents.
  • STL: Always prefer STL solutions when available, unless for very good reasons.
  • Other controversial facilities: continue, break, multiple return statements, and even goto, are all a-OK if their use is warranted and contributes to code pedagogics.
  • Legacy C code and calling conventions: Avoid. We're not writing kernel drivers. C buffer format functions do not support wide strings. new and delete completely replaces malloc() and free().
  • Pre-Standard C++ Headers (header files with ".h" ending): Avoid. The library isn't generic. Especially streams are incompatible. The STL covers all of pre-Standard C++.


Stylistic Conventions and Code Pedagogics

Naming conventions

template<typename T> class Regiment { Renderable *pRenderable; void member() {} int anotherMethod() {} Regiment& getMe() { return *this; } }; typedef Regiment<int> REGIMENT; enum MOVE_ORDER { ORDER_approach, ORDER_retreat, };
  • All classes and functions are captialised. All class members or any global variables are initiated by lower-case.
  • Genrally, CamelCase is used to separate words in identifiers. There are exceptions where appropriate and for better adherence to STL naming conventions.
  • Pointer and reference tokens ("*" and "&") belong with the identifier, not their data type. Exceptions can be made for function return types to emphasise different code structures (and because it looks a bit silly and confusing to keep return data type specifiers with the function name).
  • Identifiers names MUST be descriptive of their usage. Terseness for its own sake is dissuaded. Single-letter identifiers are prohibited except from loop control variables or obvious usages.
  • Naming should express usage not data types, wherefore Hungarian notation is dissuaded (except for lower-case "p" and "r" prefixes to indicate pointers and references, respectively) as are pseudo-Hungarian prefixes such as "C", "S", "T" etc. for classes and structs. "g_" global and "m_" member prefixes are allowed though not recommended.
  • Namespaces are always lower-cased and moderately concise.
  • Typedef and enumeration identifiers are upper-cased. Enumeration members need not be upper-cased but are always initiated by a common word in upper case (followed by an underscore to separate the enum ID marker from usage description).
  • Upper-cased identifiers use underscore to separate words in name.


Horizontal indentation and padding

namespace wb { class Weapon { std::string name; void setName( std::string const &rNewName ) { name = rNewName; } }; void Function( MOVE_ORDER order ) { AnotherFunc( std::string("Text") ); switch (order) { case ORDER_approach: break; case ORDER_retreat: break; }; } }
  • All tabs ("indentations") are translated to space. Tab width is set to 4 spaces.
  • All brackets are set on their own lines, except where silly or better aesthetics is achieved otherwise. Brackets are set on their own lines and underneath the first character of their owning keyword. This includes classes, scopes, flow control structures and namespace declarations.
  • Switch/case-structures use indented case clauses, generally with code bodies underneath the case and indented one additional tab.
  • Function signatures and calls are padded with one space. ("Call( argument )")
    • Nested calls (parenthesis inside parenthesis) are not padded. ("Call( outer(inner) )")
  • All operators are padded with a single space ("unsigned a = 10;")
  • Flow control parameters (if, for, while, catch etc) are indented one space from their keyword and use no internal padding. ("while (condition) Func();")
  • C-style cast data types are padded one space from the castee ("(unsigned) &my_int").
  • Indenting and otherwise aligning member signatures where appropriate is encouraged.


Comments and vertical spacing

/********************************************************* * Source file.cpp * * Copyright notice and documentation * * * *********************************************************/ namespace wb { //-------------------------------------------------------- class Weapon //-------------------------------------------------------- { std::string name; void setName( std::string const &rNewName ) { name = rNewName; } }; //-------------------------------------------------------- // SpecialWeapon is a specialisation of Weapon. It adds // many special capabilities over Weapon. // class SpecialWeapon //-------------------------------------------------------- { }; //-------------------------------------------------------- void Function( std::string const &rStr ) //-------------------------------------------------------- { AnotherFunc( rStr ); for (unsigned i = 0; 1 < rStr.length(); ++i) { } } }
  • Block comments are dissuaded in the main code since they interfere with temporary commenting out large blocks of code.
  • Temporary commenting out of lines generally place the comment token at column 0 of the line.
  • Class and function names are emphasised by a leading and following comment dash-line. Non-documentation (/** ... */ or /// comments) descriptions are placed underneath the first dash-line. While this might look a bit uncommon, it has proven to be highly useful for good code block separation and object identification.
  • Separate code blocks (classes, functions) are separated by three linefeeds.
  • Internal structure in code is maintained by liberal usage of blank lines -- it was a long time since we had to save vertical space on 25-line consoles.
  • Members and functions should be grouped when applicable. Grouping criteria should be obvious.


Personal tools
communication