Skip to content

2.2 Formating Code

Anthony Tan edited this page Aug 15, 2024 · 1 revision

C++

Formatting:

  • Tabs Versus Spaces:

    • Use only spaces, and indent 2 spaces at a time
    • If you are using VS Code, you can change the tab size by following these instructions:
      1. [For Windows/Linux] press Shift+Ctrl+P | [For Mac] press Shift+Command+P
      2. Search for Preferences: Configure Language Specific Settings
      3. Select the language you want to configure, in this case C++
      4. Click on Editor: Tab Size
      5. Set it to 2
  • Line Length: Each line of text in your code should be at most 80 characters long.

    • A line may exceed 80 characters if it is:
      • A comment line which is not feasible to split without harming readability, ease of cut and paste or auto-linking
      • A string literal that cannot easily be wrapped at 80 columns.
      • An include statement.
      • A header guard.
      • A using-declaration.
  • Function Declarations and Definitions: Return type on the same line as function name, parameters on the same line if they fit. Wrap parameter lists which do not fit on a single line as you would wrap arguments in a function call.

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
  DoSomething();
  ...
}
// If you have too much text to fit on one line:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) {
  DoSomething();
  ...
}
// or if you cannot fit even the first parameter:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
    Type par_name1,  // 4 space indent
    Type par_name2,
    Type par_name3) {
  DoSomething();  // 2 space indent
  ...
}
  • Floating-point Literals: Floating-point literals should always have a radix point, with digits on both sides, even if they use exponential notation.

  • Function Calls: Either write the call all on a single line, wrap the arguments at the parenthesis, or start the arguments on a new line indented by four spaces and continue at that 4 space indent. In the absence of other considerations, use the minimum number of lines, including placing multiple arguments on each line where appropriate.

  • Looping and branching statements:

    1. The components of the statement should be separated by single spaces (not line breaks).
    2. Inside the condition or iteration specifier, put one space (or a line break) between each semicolon and the next token, except if the token is a closing parenthesis or another semicolon.
    3. Inside the condition or iteration specifier, do not put a space after the opening parenthesis or before the closing parenthesis.
    4. Put any controlled statements inside blocks (i.e. use curly braces).
    5. Inside the controlled blocks, put one line break immediately after the opening brace, and one line break immediately before the closing brace.
// GOOD EXAMPLES:
if (condition) {                   // Good - no spaces inside parentheses, space before brace.
  DoOneThing();                    // Good - two-space indent.
  DoAnotherThing();
} else if (int a = f(); a != 3) {  // Good - closing brace on new line, else on same line.
  DoAThirdThing(a);
} else {
  DoNothing();
}
// Good - the same rules apply to loops.
while (condition) {
  RepeatAThing();
}
// Good - the same rules apply to loops.
do {
  RepeatAThing();
} while (condition);
// Good - the same rules apply to loops.
for (int i = 0; i < 10; ++i) {
  RepeatAThing();
}
// OK - fits on one line.
if (x == kFoo) { return new Foo(); }
// OK - braces are optional in this case.
if (x == kFoo) return new Foo();
// OK - condition fits on one line, body fits on another.
if (x == kBar)
  Bar(arg1, arg2, arg3);
while (condition) {}  // Good - `{}` indicates no logic.
while (condition) {
  // Comments are okay, too
}
while (condition) continue;  // Good - `continue` indicates no logic.
  • Pointer and Reference Expressions: No spaces around period or arrow. Pointer operators do not have trailing spaces.

  • Boolean Expressions: When you have a boolean expression that is longer than the standard line length, be consistent in how you break up the lines (e.g., always break with the last thing in the line being the logical operator).

  • Return Values: Do not needlessly surround the return expression with parentheses. Parentheses are OK to make a complex expression more readable.

  • Constructor Initializer Lists: Constructor initializer lists can be all on one line or with subsequent lines indented four spaces.

// When everything fits on one line:
MyClass::MyClass(int var) : some_var_(var) {
  DoSomething();
}
// If the signature and initializer list are not all on one line,
// you must wrap before the colon and indent 4 spaces:
MyClass::MyClass(int var)
    : some_var_(var), some_other_var_(var + 1) {
  DoSomething();
}
// When the list spans multiple lines, put each member on its own line
// and align them:
MyClass::MyClass(int var)
    : some_var_(var),             // 4 space indent
      some_other_var_(var + 1) {  // lined up
  DoSomething();
}
// As with any other code block, the close curly can be on the same
// line as the open curly, if it fits.
MyClass::MyClass(int var)
    : some_var_(var) {}
  • Operators: Assignment operators always have spaces around them. Other binary operators usually have spaces around them, but it's OK to remove spaces around factors if it improves readability. Parentheses should have no internal padding.

Extensions:

  • source.cpp, header.h

Header files:

  • In general, every .cpp file should have an associated .h file. There are some common exceptions, such as unit tests and small .cpp files containing just a main() function.

  • Self-contained Headers:

    • Header files should be self-contained (compile on their own) and end in .h. Non-header files that are meant for inclusion should end in .inc and be used sparingly. The order of header files #include should not matter for the correctness of the code.
    • All header files should be self-contained. Specifically, a header should have header guards and include all other headers it needs.
  • The #define Guard: All header files should have #define guards to prevent multiple inclusion. The format of the symbol name should be <PROJECT>_<PATH>_<FILE>_H_. To guarantee uniqueness, they should be based on the full path in a project's source tree. For example, the file foo/src/bar/baz.h in project foo should have the following guard

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif  // FOO_BAR_BAZ_H_
  • Include What You Use: 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.

  • Variables that don't change during the run time of the class can be defined in the header, except when it is a ROS parameter

Scoping:

  • Local Variables: Place a function's variables in the narrowest scope possible, initialize variables in the declaration, and as close to the first use as possible.
    • In particular, initialization should be used instead of declaration and assignment:
int i;
i = f();      // Bad -- initialization separate from declaration.
int i = f();  // Good -- declaration has initialization.

Classes:

  • Doing Work in Constructors: Avoid virtual method calls in constructors, and avoid initialization that can fail if you can't signal an error.

  • Structs vs. Classes: Use a struct only for passive objects that carry data; everything else is a class.

  • Inheritance: When a sub-class inherits from a base class, it includes the definitions of all the data and operations that the base class defines. "Interface inheritance" is inheritance from a pure abstract base class (one with no state or defined methods); all other inheritance is "implementation inheritance".

  • Operator Overloading:

    • Define operators only on your own types. More precisely, define them in the same headers, .cpp files, and namespaces as the types they operate on. That way, the operators are available wherever the type is, minimizing the risk of multiple definitions. If possible, avoid defining operators as templates, because they must satisfy this rule for any possible template arguments. If you define an operator, also define any related operators that make sense, and make sure they are defined consistently.
    • For a type T whose values can be compared for equality, define a non-member operator== and document when two values of type T are considered equal. If there is a single obvious notion of when a value t1 of type T is less than another such value t2 then you may also define operator<=>, which should be consistent with operator==. Prefer not to overload the other comparison and ordering operators.
  • Access Control: 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.

  • Declaration Order: A class definition should usually start with a public: section, followed by protected:, then private:. Within each section, prefer grouping similar kinds of declarations together, and prefer the following order:

    1. Types and type aliases (typedef, using, enum, nested structs and classes, and friend types)
    2. (Optionally, for structs only) non-static data members
    3. Static constants
    4. Factory functions
    5. Constructors and assignment operators
    6. Destructor
    7. All other functions (static and non-static member functions, and friend functions)
    8. All other data members (static and non-static)

Functions:

  • Inputs and Outputs:

    • Prefer using return values over output parameters: 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 raw pointer unless it can be null.
  • Write Short Functions: Prefer small and focused functions. We recognize that long functions are sometimes appropriate, so no hard limit is placed on functions length. If a function exceeds about 40 lines, think about whether it can be broken up without harming the structure of the program.

  • Function Overloading: Use overloaded functions (including constructors) only if a reader looking at a call site can get a good idea of what is happening without having to first figure out exactly which overload is being called.

Naming:

  • File names: Filenames should be all lowercase and can include underscores () or dashes (-). In our case, we prefer to use underscores () over dashes (-)

  • Type Names: Type names start with a capital letter and have a capital letter for each new word, with no underscores: MyExcitingClass, MyExcitingEnum. The names of all types—classes, structs, type aliases, enums, and type template parameters—have the same naming convention.

  • Variable Names: The names of variables (including function parameters) and data members are snake_case (all lowercase, with underscores between words). Data members of classes (but not structs) additionally have trailing underscores. For instance: a_local_variable, a_struct_data_member, a_class_data_member_.

  • Constant names: Variables declared constexpr or const, and whose value is fixed for the duration of the program, are named with a leading "k" followed by mixed case. Underscores can be used as separators in the rare cases where capitalization cannot be used for separation.

  • Function Names:

    • Regular functions have mixed case; accessors and mutators may be named like variables. Ordinarily, functions should start with a capital letter and have a capital letter for each new word.
    • Accessors and mutators (get and set functions) may be named like variables. These often correspond to actual member variables, but this is not required. For example, int count() and void set_count(int count).

Comments

  • Struct and Class Comments: Every non-obvious class or struct declaration should have an accompanying comment that describes what it is for and how it should be used.
// Iterates over the contents of a GargantuanTable.
// Example:
//    std::unique_ptr<GargantuanTableIterator> iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//      process(iter->key(), iter->value());
//    }
class GargantuanTableIterator {
  ...
};
  • Function Comments: Declaration comments describe use of the function (when it is non-obvious); comments at the definition of a function describe operation.
    • Function Declarations: Almost every function declaration should have comments immediately preceding it that describe what the function does and how to use it. These comments may be omitted only if the function is simple and obvious (e.g., simple accessors for obvious properties of the class). Private methods and functions declared in .cpp files are not exempt. Function comments should be written with an implied subject of This function and should start with the verb phrase; for example, "Opens the file", rather than "Open the file". In general, these comments do not describe how the function performs its task. Instead, that should be left to comments in the function definition.
    • However, do not be unnecessarily verbose or state the completely obvious
// Returns an iterator for this table, positioned at the first entry
// lexically greater than or equal to `start_word`. If there is no
// such entry, returns a null pointer. The client must not use the
// iterator after the underlying GargantuanTable has been destroyed.
//
// This method is equivalent to:
//    std::unique_ptr<Iterator> iter = table->NewIterator();
//    iter->Seek(start_word);
//    return iter;
std::unique_ptr<Iterator> GetIterator(absl::string_view start_word) const;
  • Function Definitions: If there is anything tricky about how a function does its job, the function definition should have an explanatory comment.

  • Variable Comments: In general the actual name of the variable should be descriptive enough to give a good idea of what the variable is used for. In certain cases, more comments are required.

    • Class Data Members: The purpose of each class data member (also called an instance variable or member variable) must be clear. If there are any invariants (special values, relationships between members, lifetime requirements) not clearly expressed by the type and name, they must be commented.
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;
  • Global Variables: All global variables should have a comment describing what they are, what they are used for, and (if unclear) why they need to be global.
// The total number of test cases that we run through in this regression test.
const int kNumTestCases = 6;
  • Function Argument Comments: When the meaning of a function argument is nonobvious, consider one of the following remedies:
    • If the argument is a literal constant, and the same constant is used in multiple function calls in a way that tacitly assumes they're the same, you should use a named constant to make that constraint explicit, and to guarantee that it holds.
    • Consider changing the function signature to replace a bool argument with an enum argument. This will make the argument values self-describing.
    • For functions that have several configuration options, consider defining a single class or struct to hold all the options , and pass an instance of that. This approach has several advantages. Options are referenced by name at the call site, which clarifies their meaning. It also reduces function argument count, which makes function calls easier to read and write. As an added benefit, you don't have to change call sites when you add another option.
    • Replace large or complex nested expressions with named variables.
    • As a last resort, use comments to clarify argument meanings at the call site.
// What are these arguments?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);
// versus
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
    CalculateProduct(values, options, /*completion_callback=*/nullptr);
  • TODO Comments: 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 the name/contact information of the person or issue with the best context about the problem referenced by the TODO.
// TODO: bug 12345678 - Remove this after the 2047q4 compatibility window expires.
// TODO: example.com/my-design-doc - Manually fix up this code the next time it's touched.
// TODO(bug 12345678): Update this list after the Foo service is turned down.
// TODO(John): Use a "\*" here for concatenation operator.

Others

  • Exceptions: We do not use C++ exceptions (explanation)

  • sizeof: Prefer sizeof(varname) to sizeof(type) (explanation)

  • Switch statements: If not conditional on an enumerated value, switch statements should always have a default case (in the case of an enumerated value, the compiler will warn you if any values are not handled). If the default case should never execute, treat this as an error. For example:

Python