Difference between revisions of "C++ style guide"

From KlayGE
Jump to: navigation, search
(Created page with "== Background == C++ is the main development language used by KlayGE. As every C++ programmer knows, the language has many powerful features, but this power brings with it co...")
(No difference)

Revision as of 02:31, 30 April 2012

Background

C++ is the main development language used by KlayGE. As every C++ programmer knows, the language has many powerful features, but this power brings with it complexity, which in turn can make code more bug-prone and harder to read and maintain.

The goal of this guide is to manage this complexity by describing in detail the dos and don'ts of writing C++ code. These rules exist to keep the code base manageable while still allowing coders to use C++ language features productively.

Style, also known as readability, is what we call the conventions that govern our C++ code. The term Style is a bit of a misnomer, since these conventions cover far more than just source file formatting.

One way in which we keep the code base manageable is by enforcing consistency. It is very important that any programmer be able to look at another's code and quickly understand it. Maintaining a uniform style and following conventions means that we can more easily use "pattern-matching" to infer what various symbols are and what invariants are true about them. Creating common, required idioms and patterns makes code much easier to understand. In some cases there might be good arguments for changing certain style rules, but we nonetheless keep things as they are in order to preserve consistency.

Another issue this guide addresses is that of C++ feature bloat. C++ is a huge language with many advanced features. In some cases we constrain, or even ban, use of certain features. We do this to keep code simple and to avoid the various common errors and problems that these features can cause. This guide lists these features and explains why their use is restricted.

Note that this guide is not a C++ tutorial: we assume that the reader is familiar with the language.

Header Files

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

Correct use of header files can make a huge difference to the readability, size and performance of your code.

The following rules will guide you through the various pitfalls of using header files.

The #define Guard

All header files should have both "#define guards" and "#pragma once" to prevent multiple inclusion and speed up compiling. The format of the symbol name should be _<FILE>_HPP. For example, the file foo.hpp should have the following guard:

  1. ifndef _FOO_HPP
  2. define _FOO_HPP
  1. pragma once

...

  1. endif // _FOO_HPP

Header File Dependencies

Don't use an #include when a forward declaration would suffice.

When you include a header file you introduce a dependency that will cause your code to be recompiled whenever the header file changes. If your header file includes other header files, any change to those files will cause any code that includes your header to be recompiled. Therefore, we prefer to minimize includes, particularly includes of header files in other header files.

You can significantly reduce the number of header files you need to include in your own header files by using forward declarations. For example, if your header file uses the File class in ways that do not require access to the declaration of the File class, your header file can just forward declare class File; instead of having to #include "base/file.hpp". In KlayGE, all classes and structs have forward declarations in "KlayGE/PreDeclare.hpp". You can #include it in the front of your .hpp.

How can we use a class Foo in a header file without access to its definition?

  • We can declare data members of type Foo* or Foo&.
  • We can declare (but not define) functions with arguments, and/or return values, of type Foo. (One exception is if an argument Foo or const Foo& has a non-explicit, one-argument constructor, in which case we need the full definition to support automatic type conversion.)
  • We can declare static data members of type Foo. This is because static data members are defined outside the class definition.

On the other hand, you must include the header file for Foo if your class subclasses Foo or has a data member of type Foo.

Sometimes it makes sense to have pointer (or better, scoped_ptr) members instead of object members. However, this complicates code readability and imposes a performance penalty, so avoid doing this transformation if the only purpose is to minimize includes in header files.

Of course, .cpp files typically do require the definitions of the classes they use, and usually have to include several header files.

Note: If you use a symbol Foo in your source file, you should bring in a definition for Foo yourself, either via an #include or via a forward declaration. Do not depend on the symbol being brought in transitively via headers not directly included. One exception is if Foo is used in myfile.cpp, it's ok to #include (or forward-declare) Foo in myfile.hpp, instead of myfile.cpp.

Inline Functions

Define functions inline only when they are small, say, 10 lines or less.

Definition:

You can declare functions in a way that allows the compiler to expand them inline rather than calling them through the usual function call mechanism.

Pros:

Inlining a function can generate more efficient object code, as long as the inlined function is small. Feel free to inline accessors and mutators, and other short, performance-critical functions.

Cons:

Overuse of inlining can actually make programs 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.

Decision:

A decent rule of thumb is to not inline a function if it is more than 10 lines long. Beware of destructors, which are often longer than they appear because of implicit member- and base-destructor calls!

Another useful rule of thumb: it's typically not cost effective to inline functions with loops or switch statements (unless, in the common case, the loop or switch statement is never executed).

It is important to know that functions are not always inlined even if they are declared as such; for example, virtual and recursive functions are not normally inlined. Usually recursive functions should not be inline. The main reason for making a virtual function inline is to place its definition in the class, either for convenience or to document its behavior, e.g., for accessors and mutators.

Function Parameter Ordering

When defining a function, parameter order is: inputs, then outputs.

Parameters to C/C++ functions are either input to the function, output from the function, or both. Input parameters are usually values or const references, while output and input/output parameters will be non-const pointers. 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 (often classes/structs) muddy the waters, and, as always, consistency with related functions may require you to bend the rule.

Names and Order of Includes

Use standard order for readability and to avoid hidden dependencies: KlayGE's .hpp, C library, C++ library, other libraries' .hpp, your project's .hpp.

All of KlayGE's public header files are in "Include/KlayGE" directory, which is in the include path. Use it without UNIX directory shortcuts . (the current directory) or .. (the parent directory). For example, KlayGE/foo.hpp should be included as

#include <KlayGE/foo.hpp>

In dir/foo.cc or dir/foo_test.cc, whose main purpose is to implement or test the stuff in dir2/foo2.h, order your includes as follows:

  1. KlayGE/KlayGE.hpp
  2. dir2/foo2.h (preferred location — see details below).
  3. C system files.
  4. C++ system files.
  5. Other libraries' .hpp files.
  6. Your project's .hpp files.

With the preferred ordering, if dir/foo2.hpp omits any necessary includes, the build of dir/foo.cpp or dir/foo_test.cpp will break. Thus, this rule ensures that build breaks show up first for the people working on these files, not for innocent people in other packages.

dir/foo.cpp and dir2/foo2.hpp are often in the same directory (e.g. base/basictypes_test.cpp and base/basictypes.hpp), but can be in different directories too.

For example, the includes in KlayGE/src/foo.cpp might look like this:

#include <KlayGE/KlayGE.hpp>
#include <KlayGE/Math.hpp>
#include <KlayGE/ResLoader.hpp>

#include <vector>

#include <boost/shared_ptr.hpp>

#include <KlayGE/foo.hpp>