DarkRadiant coding standards: Difference between revisions

From The DarkMod Wiki
Jump to navigationJump to search
OrbWeaver (talk | contribs)
Orbweaver (talk | contribs)
Additional shared ptr typedef option
 
(13 intermediate revisions by 5 users not shown)
Line 1: Line 1:
Although DarkRadiant was forked from GTKRadiant, there are new '''coding standards''' which should be used for new code, in order to maximise readability for developers.
Although DarkRadiant was forked from GTKRadiant, the style of legacy GTKRadiant code should ''not'' be used as an example for new code. Therefore these '''coding standards''' should be used for new code, in order to maximise readability for developers.
 
==Indentation==
 
* Prefer 4-space indentation in new code. However, when editing legacy code which is using a different indentation style, it is acceptable to maintain consistency with the legacy code rather than reformatting. Try to avoid mixing tabs and spaces within the same file as this can break alignment when viewing diffs.
* Function and method bodies should use either the [https://en.wikipedia.org/wiki/Indentation_style#Allman_style Allman] or [https://en.wikipedia.org/wiki/Indentation_style#K&R_styleK&R K&R] brace styles, and where possible the opening brace should be in column 0. This enables developers using Vim to make use of the "[[" shortcut to jump to the beginning of the method.


==Naming conventions==
==Naming conventions==
Line 6: Line 11:
* Class names should begin with a capital letter and capitalise each word: '''RenderablePicoModel''', '''TextMenuItem'''
* Class names should begin with a capital letter and capitalise each word: '''RenderablePicoModel''', '''TextMenuItem'''
* Classes represent "things", therefore they should be named after nouns not verbs: '''ModuleLoader''' rather than '''LoadModule'''.
* Classes represent "things", therefore they should be named after nouns not verbs: '''ModuleLoader''' rather than '''LoadModule'''.
* Each new class should be contained within its own file pair: '''MyClass''' is contained in '''MyClass.h''' and '''MyClass.cpp'''. This rule does not apply to tightly-bound local classes, such as a functor class which is used by only a single method in a .cpp file. It is also acceptable to define trivial data structures returned by methods in the header file declaring the method, if that data structure is not used elsewhere.
* Each class should be in a file pair with file names matching the class name: '''MyClass''' is contained in '''MyClass.h''' and '''MyClass.cpp'''. Local classes used in a single .cpp file may be defined within that .cpp file. It is also acceptable to define trivial data structures returned by methods in the header file declaring the method, if that data structure is not used elsewhere.
* Method names and local variables should start with a small letter.
* Method names and local variables should start with a lowercase letter, with subsequent words using camelcase: '''start()''', '''getDataStructure()'''. With wxWidgets, however, the convention is different — methods begin with capital letters just like class names. When writing code that is tightly bound to wxWidgets, it is acceptable to use this same style for methods (but please be consistent).
* Member variables should begin with an underscore: '''_widget'''. Do not use the "m_name" convention in new code, as this is harder to read.
* Member variables should begin with an underscore: '''_widget'''. Do not use the "m_name" convention in new code, as this is harder to read.
===Typedefs===
Using the typedef keyword can improve readability and save typing by providing a simple name for a more complex data structure, however inappropriate use can also reduce readibility by forcing the reader to look up numerous typedefs which do no more than rename a standard type.
For example:
typedef std::vector<std::string> StringList
is a good typedef, because it is obvious what the type means, it allows easy editing if it were required to change it to a std::list rather than a vector (for example), and it makes it easy to work with derived types ('''StringList::iterator''' rather than '''std::vector<std::string>::iterator''').
On the other hand,
typedef float ShaderScaleParameter
is a bad typedef. It provides no improved readability over simply using the primitive type, and gives the impression to a reader unfamiliar with the code that a more complex data structure is being used.
* Never use typedefs to rename a standard type (unless there is a specific purpose, like providing a standard public typedef as part of a functor interface).


==General code structure==
==General code structure==


* Always use Object-Oriented design methods wherever possible. Think in terms of objects which define methods to act on those objects, rather than C-style functions that process data structures passed as arguments. Non-member functions are OK for minor tasks, as long as they are within a namespace rather than at global scope.
* Always use Object-Oriented design methods wherever possible. Think in terms of objects which define methods to act on those objects, rather than C-style functions that process data structures passed as arguments. Non-member functions are OK for minor tasks, as long as they are within a namespace rather than at global scope.
* Always use STL or Boost objects, rather than home-grown reinventions of the same thing. Use '''std::string''' not '''CopiedString''', and '''std::map''' in favour of '''HashedCache'''. Classes that use these home-grown versions should be modified so as not to use them if at all possible, and eventually they will be removed.
* Never use global variables (non-constants at file or global scope). Constants at file scope are acceptable, either as primitive types or composite data structures (e.g. vectors or maps) whose content never needs to change.
* Never use global variables.
 
===Data types and objects===
 
* Always use STL or other library objects (e.g. wxWidgets), rather than home-grown reinventions of the same thing. Use '''std::string''' for string variables, and '''std::map''' in favour of '''HashedCache'''. Classes that use these home-grown versions should be modified so as not to use them if at all possible, and eventually they will be removed.
* Note that ''we are no longer using Boost'', since modern C++ provides almost all of the Boost facilities we previously needed (shared pointers, std::function etc), and avoiding Boost greatly simplifies the Windows build. If you are developing on a platform where Boost is easily available, be careful not to accidentally add a new Boost usage which will break Windows compilation.
* Do not use '''const char*''' objects except where necessary to pass to a library function (such as the C-based GTK library), or where a string constant is needed. Functions that accept or return strings should use '''std::string''' instead.
 
=== Data allocation ===
Use '''std::shared_ptr'''s (or '''std::unqiue_ptr''') wherever you need to allocate objects on the heap, as these objects auto-destruct when the last ptr instance is destroyed. Use '''new''' only if you need tight control of when the object should be removed from the heap, or to instantiate GUI classes that auto-destruct in their callbacks.
 
A shared ptr should be named like this (Ptr suffix):
typedef std::shared_ptr<MyClass> MyClassPtr;
 
It is also acceptable to add the shared ptr typedef inside the class itself, e.g.
 
<nowiki>
class MyClass
{
    using Ptr = std::shared_ptr<MyClass>
};</nowiki>
 
which allows the type to be referenced as '''MyClass::Ptr'''.


===Static variables===
===Static variables===
Line 34: Line 77:
Exactly where and how to place comments is a matter of personal preference. However, at the very least, ''all'' public methods and functions should be commented in their associated header file, with details of their parameters, return value and function, along with any other important information (such as prerequisites for calling the function, who is responsible for destroying returned data etc.).
Exactly where and how to place comments is a matter of personal preference. However, at the very least, ''all'' public methods and functions should be commented in their associated header file, with details of their parameters, return value and function, along with any other important information (such as prerequisites for calling the function, who is responsible for destroying returned data etc.).


==Module interface==
{{darkradiant-internal|sort=Coding}}
 
The design of DarkRadiant is a modular one. Each module encapsulates a certain amount of functionality, such as the '''entity''' module which provides entity creation and rendering, and the '''model''' module which loads modules from ASE, LWO or MD5 files. Each module implements an interface which is defined in an "i*.h" file in the '''include''' directory (such as "imodel.h" and "ientity.h"). There is also a '''Radiant''' module whose interface is specified in "qerplugin.h".
 
Modules access each other using inline functions defined in the interface header file, such as '''GlobalRadiant()''' or '''GlobalShaderCache()'''. These functions return a reference (or pointer) to a static instance of the corresponding module. The initialisation order of modules is important, and this is controlled via the use of a '''ModuleRef''' template, which instantiates and maintains a reference to a singleton module instance, and "Dependencies classes" which are empty classes that inherit from a number of ModuleRef templates, thus ensuring that each ModuleRef is constructed before the Dependencies class itself.

Latest revision as of 08:31, 26 August 2020

Although DarkRadiant was forked from GTKRadiant, the style of legacy GTKRadiant code should not be used as an example for new code. Therefore these coding standards should be used for new code, in order to maximise readability for developers.

Indentation

  • Prefer 4-space indentation in new code. However, when editing legacy code which is using a different indentation style, it is acceptable to maintain consistency with the legacy code rather than reformatting. Try to avoid mixing tabs and spaces within the same file as this can break alignment when viewing diffs.
  • Function and method bodies should use either the Allman or K&R brace styles, and where possible the opening brace should be in column 0. This enables developers using Vim to make use of the "[[" shortcut to jump to the beginning of the method.

Naming conventions

  • All new code should be in a namespace, such as ui for UI-related code or model for code dealing with models.
  • Class names should begin with a capital letter and capitalise each word: RenderablePicoModel, TextMenuItem
  • Classes represent "things", therefore they should be named after nouns not verbs: ModuleLoader rather than LoadModule.
  • Each class should be in a file pair with file names matching the class name: MyClass is contained in MyClass.h and MyClass.cpp. Local classes used in a single .cpp file may be defined within that .cpp file. It is also acceptable to define trivial data structures returned by methods in the header file declaring the method, if that data structure is not used elsewhere.
  • Method names and local variables should start with a lowercase letter, with subsequent words using camelcase: start(), getDataStructure(). With wxWidgets, however, the convention is different — methods begin with capital letters just like class names. When writing code that is tightly bound to wxWidgets, it is acceptable to use this same style for methods (but please be consistent).
  • Member variables should begin with an underscore: _widget. Do not use the "m_name" convention in new code, as this is harder to read.

Typedefs

Using the typedef keyword can improve readability and save typing by providing a simple name for a more complex data structure, however inappropriate use can also reduce readibility by forcing the reader to look up numerous typedefs which do no more than rename a standard type.

For example:

typedef std::vector<std::string> StringList

is a good typedef, because it is obvious what the type means, it allows easy editing if it were required to change it to a std::list rather than a vector (for example), and it makes it easy to work with derived types (StringList::iterator rather than std::vector<std::string>::iterator).

On the other hand,

typedef float ShaderScaleParameter

is a bad typedef. It provides no improved readability over simply using the primitive type, and gives the impression to a reader unfamiliar with the code that a more complex data structure is being used.

  • Never use typedefs to rename a standard type (unless there is a specific purpose, like providing a standard public typedef as part of a functor interface).

General code structure

  • Always use Object-Oriented design methods wherever possible. Think in terms of objects which define methods to act on those objects, rather than C-style functions that process data structures passed as arguments. Non-member functions are OK for minor tasks, as long as they are within a namespace rather than at global scope.
  • Never use global variables (non-constants at file or global scope). Constants at file scope are acceptable, either as primitive types or composite data structures (e.g. vectors or maps) whose content never needs to change.

Data types and objects

  • Always use STL or other library objects (e.g. wxWidgets), rather than home-grown reinventions of the same thing. Use std::string for string variables, and std::map in favour of HashedCache. Classes that use these home-grown versions should be modified so as not to use them if at all possible, and eventually they will be removed.
  • Note that we are no longer using Boost, since modern C++ provides almost all of the Boost facilities we previously needed (shared pointers, std::function etc), and avoiding Boost greatly simplifies the Windows build. If you are developing on a platform where Boost is easily available, be careful not to accidentally add a new Boost usage which will break Windows compilation.
  • Do not use const char* objects except where necessary to pass to a library function (such as the C-based GTK library), or where a string constant is needed. Functions that accept or return strings should use std::string instead.

Data allocation

Use std::shared_ptrs (or std::unqiue_ptr) wherever you need to allocate objects on the heap, as these objects auto-destruct when the last ptr instance is destroyed. Use new only if you need tight control of when the object should be removed from the heap, or to instantiate GUI classes that auto-destruct in their callbacks.

A shared ptr should be named like this (Ptr suffix):

typedef std::shared_ptr<MyClass> MyClassPtr;

It is also acceptable to add the shared ptr typedef inside the class itself, e.g.

class MyClass
{
    using Ptr = std::shared_ptr<MyClass>
};

which allows the type to be referenced as MyClass::Ptr.

Static variables

If a static variable is required, encapsulate it within a small method that returns a reference to the static, e.g.

MyObject& staticInstance() {
    static MyObject _instance;
    return _instance;
}

This ensures that the static instance will be initialised when the method is first called, which avoids problems with the undefinability of static initialisation.

Access

  • All data members should be private, except in data structures which provide no methods (other than constructors).
  • Members which need to be accessed externally should be provided with get/set methods. This is an important part of separating interface from implementation, since a get/set method might actually modify more than one member variable, or even perform a calculation without referencing members at all.

Comments

Exactly where and how to place comments is a matter of personal preference. However, at the very least, all public methods and functions should be commented in their associated header file, with details of their parameters, return value and function, along with any other important information (such as prerequisites for calling the function, who is responsible for destroying returned data etc.).