V-Gears C++ Style GuideAs Q-Gears started growing in size, it became more and more obviuos that it was the work of multiple developers, across several years. There were differences in style, namespace use, variable and function name...
While no one of those different styles was inherently superior to any other, it made the project inelegant in the best of cases, and consfusing and non-interoperable in the worst of them.
When it became V-Gears, every piece of code was, in different degrees, refactored, documented, and simplified. The same style was applied across the whole project, and it was a huge amount of work.
To avoid having to go through all of that again, this guide was created.
This guide describes the rules for code styling for V-Gears, and, as far as possible, explains why a rule is chosen over other options.
Currently, the code should target C++14.
Over time, the project and this guide will be upgraded to newer versions of C++. This will be done when and only when it's safe enough to upgrade without compatibility issues. Until then, any contributions must be ensured to compile and run with C++14.
Do not use non-standard extensions, such as GNU extensions. They are very specific to compilers and can jeopardize current or furure portability efforts.
Source files must have the extension .cpp.
In general, every .cpp file should have an associated .h header file. There are some exceptions to this rule:
main() functions.Correct use of header files can make a huge difference to the readability and size of the project, and can also save a lot of time during compilation.
The following rules will guide you through the various pitfalls of using header files.
All header files should be self-contained. Users and refactoring tools should not have to adhere to special conditions to include the header.
When a header declares inline functions or templates that clients of the header will instantiate, the inline functions and templates must also have definitions in the header, either directly or in files it includes.
All header files should have an include guard. The prefered methoud to do is is with #pragma once.
This directive is mature enough to favor it over #ifndef guards, which are more complex, and can conflict with one another in a large enough project.
If a source or header file refers to a symbol defined elsewhere, the file should directly include a header file which properly intends to provide a declaration or definition of that symbol. It should not include header files for any other reason.
Do not rely on transitive inclusions. This can be hard to debug when one of the transitive files gets refactored.
Avoid using forward declarations where possible.
A "forward declaration" is a declaration of an entity without an associated definition. They can save on compile time, but they can be tricky for several reasons:
The only acceptable reason to use a forward declaration is to prevent an include loop i.e. two files including each other. Even then, an include loop i'ts usually a sign of deeper problems, and the affected code probably can and should be refactored to avoid the loop and the forward declaration alltpgether.
As a rule of thumb, don't inline functions. Let your compiler do it's thing, it will inline function as it sees fit.
In any case, define functions inline only when they are small (about 10 lines or fewer).
Overuse of inlining can actually make the program slower. Depending on a function's size, inlining it can cause the code size to increase or decrease. Inlining a very small accessor function will usually decrease code size while inlining a very large function can dramatically increase code size. On modern processors smaller code usually runs faster due to better use of the instruction cache.
Always avoid inlining functions with loops or switch statements.
Include headers in the following order, without whitelines behind them:
1 - C++ standard library headers 2 - C system headers 3 - Other libraries' headers 4 - Related header 5 - Other V-Gears headers.
There is no particular reason as to why this order has been chosen, save for consistency with the rest of the project.
Don't use relative paths for the include directives.
The use of namespaces is neither prefered nor discouraged.
If a collection of files, classes, functions belong to a well-defined and well-limited part of the application, feel free to include them in a namespace.
When using namespaces, try to follow these directives:
When definitions in a .cpp file do not need to be referenced outside that file, give them internal linkage by declaring them static. Do not use either of these constructs in .h files.
Internal linkage should be used whenever possible in .cpp for all code that does not need to be referenced elsewhere. Do not use internal linkage in .h (it should not bee needed).
Whenever possible, try to avoid static functions outside of classes. Try to create them as static methods in the class they are related to. It is allowed to create a class to hold several related static methods.
Avoid using global variables functions as much as possible, use them only as a last resort. They are incredibly hard to keep track of.
Place a function's variables in the narrowest scope possible, and initialize variables in the declaration.
This section describes several rules to take into account when writing a class.
Avoid initialization that can fail if an error can't be signaled (terminating the application can be a good signal, it the error is non-recoverable). Avoid calling virtual functions from constructors. When neccesary, implement a separate initialization method and call it from the constructor.
Do not define implicit conversions. Use the explicit keyword for conversion operators and single-argument constructors.
The struct and class keywords behave almost identically in C++. When trying to choose between them, use these considerations.
Structs should be used for (mostly) passive for data. Most or all fields must be public. If there are methods, they must be accessors or very simple methods. For example, it's OK to have an array or a vector as a member, and a method to fetch an element at a specific position.
For anything more complex, use classes. In classes, fields must be private and protected, and have the required accessor methods.
Use a struct only for passive objects that carry data; everything else is a class.
All inheritance should be public.
Limit the use of protected to those member functions that might need to be accessed from subclasses.
Explicitly annotate overrides of virtual functions or virtual destructors with exactly one of an override or (less frequently) final specifier.
Overload operators judiciously. Do not use user-defined literals.
Do not overload &&, ||, , (comma), or unary &. Do not overload operator"", i.e., do not introduce user-defined literals. Do not use any such literals provided by others (including the standard library).
Make classes' data members private, unless they are constants. This simplifies reasoning about invariants, at the cost of some easy boilerplate in the form of accessors (usually const) if necessary.
Group similar declarations together, placing public parts earlier.
A class definition should usually start with a public: section, followed by protected:, then private:. Omit sections that would be empty.
Within each section, prefer grouping similar kinds of declarations together, and prefer the following order:
typedef, using, enum, nested structs and classes, and friend types)The output of a C++ function is naturally provided via a return value and sometimes via output parameters (or in/out parameters).
Prefer using return values over output parameters whenever possible: they improve readability, and often provide the same or better performance.
Prefer to return by value or, failing that, return by reference. Avoid returning a pointer unless it can be nullable.
Parameters are either inputs to the function, outputs from the function, or both. Non-optional input parameters should usually be values or const references, while non-optional output and input/output parameters should usually be references (which cannot be null).
When ordering function parameters, put all input-only parameters before any output parameters. In particular, do not add new parameters to the end of the function just because they are new; place new input-only parameters before the output parameters. This is not a hard-and-fast rule. Parameters that are both input and output muddy the waters, and, as always, consistency with related functions may require you to bend the rule.
Prefer small and focused functions.
While there is no hard limit is placed on functions length, if a function exceeds about 80 lines, think about whether it can be broken up without harming the structure of the program.
Short functions are easier to debug, modify or refactor, and keep the code more readable and easier to maintain.
Default arguments are allowed on non-virtual functions when the default is guaranteed to always have the same value.
The use of friend classes and functionsis allowed, within reason, but it's best avoided whenever possible.
Friends extend, but do not break, the encapsulation boundary of a class. In some cases this is better than making a member public when you want to give only one other class access to it. However, most classes should interact with other classes solely through their public members.
The use of C++ exceptions is allowed. They allow higher levels of an application to decide how to handle failures in deeply nested functions, without the obscuring and error-prone bookkeeping of error codes.
Some caution must be taken when using exceptions:
throw statement is added to an existing function, all of its transitive callers must be examined. Either they must make at least the basic exception safety guarantee, or they must never catch the exception and be happy with the program terminating as a result. For instance, if f() calls g() calls h(), and h throws an exception that f catches, g has to be careful or it may not clean up properly.noexcept when it is useful and correct.RTTI allows a programmer to query the C++ class of an object at run-time. This is done by use of typeid or dynamic_cast.
Avoid them whenever possible, except for tests.
Use C++-style casts like static_cast<float>(double_value), or brace initialization for conversion of arithmetic types like int64_t y = int64_t{1} << 42. Do not use cast formats like (int)x unless the cast is to void. You may use cast formats like `T(x)` only when `T` is a class type.
Use streams where appropriate, and stick to "simple" usages. Overload << for streaming only for types representing values, and write only the user-visible value, not any implementation details.
Overload << as a streaming operator for your type only if your type represents a value, and << writes out a human-readable string representation of that value. Avoid exposing implementation details in the output of <<; if you need to print object internals for debugging, use named functions instead (a method named DebugString() is the most common convention).
Use the prefix form (++ i) of the increment and decrement operators unless you need postfix (i ++) semantics. This can result in a slightly better performance of the algorithm.
In APIs, use const whenever it makes sense. constexpr is a better choice for some uses of const.
Declared variables and parameters can be preceded by the keyword const to indicate the variables are not changed (e.g., const int foo). Class functions can have the const qualifier to indicate the function does not change the state of the class member variables (e.g., class Foo { int Bar(char c) const; };).
The const keyword must be before the type (const int *foo instead of int const *foo). Putting the const first is arguably more readable, since it follows English in putting the "adjective" (const) before the "noun" (int).
Use constexpr to define true constants or to ensure constant initialization.
Code should be 64-bit and 32-bit friendly. Bear in mind problems of printing, comparisons, and structure alignment.
Avoid defining macros, especially in headers; prefer inline functions, enums, and const variables.
Macros mean that the code you see is not the same as the code the compiler sees.
Use nullptr for pointers, and '\0' for chars (and not the 0 literal).
For pointers (address values), use nullptr, as this provides type-safety.
Use '\0' for the null character. Using the correct type makes the code more readable.
Prefer sizeof(varname) to sizeof(type).
Use type deduction only for local variables, where the scope is short and simple enough to be easily understood.
Abusing type deduction makes the code harder to read, and IDEs sometimes can't determine deducted types.
Function template argument deduction is almost always OK. Type deduction is the expected default way of interacting with function templates, because it allows function templates to act like infinite sets of ordinary function overloads. Consequently, function templates are almost always designed so that template argument deduction is clear and safe, or doesn't compile.
Use class template argument deduction only with templates that have explicitly opted into supporting it.
Avoid complicated template programming.
The techniques used in template metaprogramming are often obscure to anyone but language experts. Code that uses templates in complicated ways is often unreadable, and is hard to debug or maintain.
It's OK to use libraries from the Boost library collection.
The most important consistency rules are those that govern naming. The style of a name immediately informs us what sort of thing the named entity is: a type, a variable, a function, a constant, a macro, etc., without requiring us to search for the declaration of that entity. The pattern-matching engine in our brains relies a great deal on these naming rules.
Naming rules are pretty arbitrary, and no one is inherently better than other, but for V-Gears, these are the choosen rules.
Filenames start with a capital letter and have a capital letter for each new word, with no underscores:
MyUsefulClass.cppANiceLibrary.hUtilities.cppFor test files, use the same scheme, but with test_ before the name:
test_MyUsefulClass.cppC++ source files should end in .cpp and header files should end in .h. Files that rely on being textually included at specific points should end in .inc, but try to avoid them.
Type names start with a capital letter and have a capital letter for each new word, with no underscores:
MyExcitingClassMyExcitingEnum.The names of all types — classes, structs, type aliases, enums, and type template parameters — have the same naming convention. Type names should start with a capital letter and have a capital letter for each new word. No underscores.
The names of variables (including function parameters) and data members are all lowercase, with underscores between words. Data members of classes (but not structs) additionally have trailing underscores:
a_local_variable,some_public_struct_data_member,Protected and private data members of classes and structs foollow the same convention, but they also end with a trailing underscore:
private_class_data_member_.Variables declared constexpr or const, and whose value is fixed for the duration of the program, are named with uppercase, with the words separated by underscores:
const int DAYS_IN_A_WEEK = 7;
const int APRIL = 4;
Function names start with a capital letter and have a capital letter for each new word, with no underscores. This is true for regular functions, class and struct methods (public or private), accessors...:
AddTableEntry() MyClass::DeleteUrl() OpenFileOrDie()
Namespaces start with a capital letter and have a capital letter for each new word, with no underscores. If the name is an acronym, all letters can be uppercase.
namespace FieldTools = 7;
using namespace ACME;
Enumerators (for both scoped and unscoped enums) should be named like constants, uppercase, with the words separated by underscores:
enum class URL_TABLE_ERROR { OK = 0, OUT_OF_MEMORY, MALFORMED_INPUT, };
If the use of macros can't be avoided, name them like constants: Uppercase, with the words separated by underscores.
This section applies only to individual inline comments. For code documentation, see the Code Documentation section.
Always write comments in tricky, non-obvious, interesting, or important parts of the code.
For comments inside the code, use either the // or /* */ syntax. They both can be used together, there is no need for consistency in this regard.
Pay attention to punctuation, spelling, and grammar; it is easier to read well-written comments than badly written ones.
Comments should be as readable as narrative text, with proper capitalization and punctuation. In many cases, complete sentences are more readable than sentence fragments.
Use TODO comments for code that is temporary, a short-term solution, or good-enough but not perfect.
TODOs should include the string TODO in all caps, followed by an explanation of what the code should be doing, ant the why of the TODO. A todo comment can expand serveral lines
// TODO: This always give fixed values.
// TODO: Why is this used twice?
// TODO: Move depth calculation to flevelBackgroundLoader maybe? and let
// Backgorund2D only handle 0 <= depth <= 1 or so?
If your TODO is of the form "At a future date do something" make sure that you either include a very specific date ("Fix by November 2005") or a very specific event ("Remove this code when all clients can handle XML responses.").
Every file should contain copiright and license boilerplate in the very first line, exactly like this:
/*
* Copyright (C) 2022 The V-Gears Team
*
* This file is part of V-Gears
*
* V-Gears is free software: you can redistribute it and/or modify it under
* terms of the GNU General Public License as published by the Free Software
* Foundation, version 3.0 (GPLv3) of the License.
*
* V-Gears is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
Every piece of code must be documented. The project uses Doxygen as a documentation generator, but the coments themselves are in JavaDoc format.
Do not duplicate comments in both the .h and the .cpp. Duplicated comments diverge. When a function is declared in a .h file, document it there.
Every class, struct, enum, enum item constant and class member must be preceded by a codeblock. If a one-liner is enough to describe what it's for, then so be it. If further explanation is needed, leave an empty line and add an explanation. Examples:
/**
* Used to store coordinates.
*/
struct Coordinates{...}
/**
* This class does a tricky thing.
*
* It represents something, but should not be used used when some other thing.
*/
class TrickyClass{...}
/**
* Represents a field map model.
*
*/
class Model{
...
private:
/**
* The height of the model.
*/
height_;
}
/**
* The scale factor for line point coordinates.
*
* When a LINE opcode is found in the game scripts, the line points X and Y coordinates
* must be scaled down by this factor.
*/
static float LINE_SCALE_FACTOR;
/**
* How to scroll the background.
*/
enum SCROLL_TYPE{
/**
* Don't scroll the background.
*/
NONE,
/**
* Linearly scroll.
*
* May seem unnatural in scripted sequences, but it's OK for
* character tracking.
*/
LINEAR,
...
};
Each function or class method (public, protected or private) declaration must be preceded by it's own Javadoc block. Like this:
/**
* Processes a conditional jump instruction.
*
* Checks if the condition is a function or a comparison, and adds the function to the stack.
*
* @param[in] function Function to process. Unused.
* @param[out] stack Function stack. The instruction will be added here.
* @param[in] engine Engine. Unused.
* @param[in,out] code_gen Code generator to append lines.
*/
virtual void ProcessInst(Function& function, ValueStack& stack, Engine* engine, CodeGenerator* code_gen) override;
/**
* Checks the model visibility.
*
* @return True if the model is visible, false if it's invisible.
*/
virtual bool IsVisible() const;
If one line is not enough to describe what the function it's for, leave an empty line and add an explanation. Always document every parameter the function takes, indicating it's direction ([in], [out] or [in,out]) and the return value, if any
Coding style and formatting are pretty arbitrary, but a project is much easier to follow if everyone uses the same style.
Each line of text in your code should be at most 100 characters long. This is a reasonable compromise between the old standard of 80 characters and what moders screens offer.
Non-ASCII characters should be avoided, and must use UTF-8 formatting when used.
Use only spaces, and indent 4 spaces at a time. Do not use tabs.
Return type on the same line as function name, parameters on the same line if they fit. Wrap parameter lists which do not fit to a line on their own, indented by 2 extra spaces.
Functions look like this:
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
DoSomething();
...
}
If there is too much text to fit on one line:
ReturnType ClassName::ReallyLongFunctionName(
Type par\_name1, Type par\_name2, Type par\_name3
){
DoSomething();
...
}
Some points to note:
In an if statement, including its optional else if and else clauses, put one space between the if and the opening parenthesis, but no spaces between the parentheses and the condition or initializer or between the closing parenthesis and the curly brace.
if(condition){...} // Bad - space missing after IF.
if ( condition ){...} // Bad - space between the parentheses and the condition.
if (condition) {...} // Bad - space before brace.
if (condition){...} // Good
Use curly braces for the controlled statements following if, else if and else. Break the line immediately after the opening brace, and immediately before the closing brace. A subsequent else, if any, appears on a new line after the preceding closing brace.
if (condition){
DoOneThing();
DoAnotherThing();
}
else if (int a = f();){
DoAThirdThing(a);
}
else{
DoNothing();
}
If only one statement depends on a condition (if, else if or else) it's Ok to omit the braces. Also, if the condition and the statement fit on the same line, they can be written together:
if (condition){
DoOneThing();
DoAnotherThing();
}
else if (a != 3 && GetResultOfSomeVeryVeryLongFunctionCall(with, lots, of, arguments) == false)
DoAThirdThing(a);
else DoNothing();
Use the same rules that for conditionals.
When you have a boolean expression that is longer than the maximum line length (100 characters), separate the expresion to a new line, indented with 2 characters. If even this is not enough, separate the conditions in lines, breaking them before the operator
if (
this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another
){
...
}
if (
this_one_thing > this_other_thing && a_third_thing == a_fourth_thing
&& yet_another && last_one
){
...
}
Do not needlessly surround the return expression with parentheses.
You may choose between =, (), and {}; the following are all correct:
int x = 3;
int x(3);
int x{3};
std::string name = "Some Name";
std::string name("Some Name");
std::string name{"Some Name"};
The hash mark that starts a preprocessor directive should always be at the beginning of the line.
Even when preprocessor directives are within the body of indented code, the directives should start at the beginning of the line.
if (lopsided_score) {
#if DISASTER\_PENDING // Correct -- Starts at beginning of line
DropEverything();
# if NOTIFY // OK but not required -- Spaces after #
NotifyClient();
# endif
#endif
BackToNormal();
}
Sections in public, protected and private order, each indented four spaces. Leave an empty line before the public, protected and private keywords, and before each function or member.
class MyClass: public OtherClass {
public:
MyClass();
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
private:
bool SomeInternalFunction();
int some_var_;
};
Use of horizontal whitespace depends on location. Never put trailing whitespace at the end of a line.
int i = 0; // One or two spaces before end-of-line comments.
void f(bool b){ // Open braces should never have a space before them.
...
}
int i = 0; // Semicolons have no space before them.
int x[] = {0}; // Don't use spaces inside braces or brackets.
// Spaces after (but not before) the colon in inheritance and initializer lists.
class Foo: public Bar {
public:
// For inline function implementations, put spaces between the braces
// and the implementation itself.
Foo(int b) : Bar(), baz\_(b){}
void Reset(){baz_ = 0;}
...
Adding trailing whitespace can cause extra work for others editing the same file, when they merge, as can removing existing trailing whitespace. So: Don't introduce trailing whitespace.
// All operators always have spaces around them.
x = 0;
v = w * x + y / z;
v = w\*x + y/z;
v = w * (x + z);
x = -5;
++ x;
if (x && !y){}
// No spaces inside the angle brackets (< and >), before
// <, or between >( in a cast
std::vector<std::string> x;
y = static\_cast<char\*>(x);
// Spaces between type and pointer are OK, but be consistent.
std::vector<char \*> x;
This file is derived from Google C++ Style Guide. While some rules are completly different, the document structure, several paragraphs and many examples are original or closely derived.