Interpretation of Google C++ Style Guide Series Part 1 (Header Files)

This series of articles is an interpretation of the original text, adding some personal insights and supplementing actual code examples to aid understanding – Original text link

C++ Version

The target version of this Google C++ style guide is C++20, so it will not cover features of C++23.

Header Files

Typically, each <span>.cc</span> file should have a corresponding <span>.h</span> file, with common exceptions including unit tests and <span>main()</span> function-only <span>.cc</span> files. Proper use of header files greatly improves code readability and the size and performance of executable files.

1. Self-Contained Header Files

Header files should be self-contained (i.e., able to be compiled independently) and have a <span>.h</span> extension. Files that need to be imported (included) but do not belong to header files should be given a <span>.inc</span> extension, but this should be avoided as much as possible.

All header files should be self-contained, meaning that users of the header file and refactoring tools do not need any special prerequisites when importing the file. Specifically, header files must have header guards and import all other header files they require. If a header file declares an inline function or template, and the user of the header file needs to instantiate these components, the header file must provide the implementation (definition) of these components directly or indirectly through imported files; do not place these implementations in another header file (e.g., <span>-inl.h</span> file) and import it; this was a past practice but is now prohibited. If all instantiation processes of a template only appear in one <span>.cc</span> file, such as using explicit instantiation, or only this <span>.cc</span> file will use the template definition, then the template definition can be placed in that file. In rare cases, files used for imports cannot be self-contained. They are usually imported in special places, such as in the middle of another file, and such files do not need to use header guards or import their prerequisites. Such files should use a <span>.inc</span> extension, but this type of file should be used sparingly; self-contained header files should be used whenever possible.

1.1 Interpretation

The core logic of Google Style is: make header files “independent units” – whether for developers or tools, simply importing the header file should allow for correct compilation without additional dependency assumptions. What problems may arise if the “self-contained header file” rule is violated?

1. Missing Header Guards

Counterexample

// bad_header.h
class MyClass { /* ... */ };

Problem

If multiple source files include this header file, it will trigger a compilation error:

error: redefinition of 'class MyClass'

2. Inline Function Implementation Not in Header File

Counterexample

// bad_inline.h
#ifndef PROJECT_BAD_INLINE_H_
#define PROJECT_BAD_INLINE_H_

inline int Add(int a, int b);  // Declare inline function

#endif  // PROJECT_BAD_INLINE_H_

// bad_inline.cc
#include "bad_inline.h"
inline int Add(int a, int b) { return a + b; }  // Implementation in .cc file

Problem

Different compilation units may see different definitions of the inline function, leading to:

  • Linking Errors: If the inline function is not defined in all compilation units that use it.
  • Inconsistent Behavior: Some compilation units use the inline version, while others use the external linkage version.

3. Template Implementation Not in Header File

Counterexample

// bad_template.h
#ifndef PROJECT_BAD_TEMPLATE_H_
#define PROJECT_BAD_TEMPLATE_H_

template <typename T>
class Vector {
    void PushBack(const T& value);  // Declared but not defined
};

// Error: Include implementation through -inl.h (prohibited by Google Style)
#include "bad_template-inl.h"

#endif  // PROJECT_BAD_TEMPLATE_H_

// bad_template-inl.h (prohibited)
template <typename T>
void Vector<T>::PushBack(const T& value) { /* ... */ }

Problem

If the user forgets to include the <span>-inl.h</span> file:

// main.cc
#include "bad_template.h"

int main() {
    Vector<int> v;
    v.PushBack(42);  // Linking error: function definition not found
    return 0;
}

4. Dependency on External Macro Definitions

Counterexample

// bad_header.h
#ifndef PROJECT_BAD_HEADER_H_
#define PROJECT_BAD_HEADER_H_

void Foo() {
    #ifdef ENABLE_FEATURE  // Depends on externally defined macro
        DoSomething();
    #endif
}

#endif  // PROJECT_BAD_HEADER_H_

Problem

The user must manually define the <span>ENABLE_FEATURE</span> macro:

// main.cc
#define ENABLE_FEATURE  // Easy to overlook
#include "bad_header.h"  // Behavior depends on the definition location of the macro

5. Implicit Dependency on Global State

Counterexample

// bad_header.h
#ifndef PROJECT_BAD_HEADER_H_
#define PROJECT_BAD_HEADER_H_

void PrintGlobalValue() {
    extern int global_value;  // Depends on undeclared global variable
    printf("%d\n", global_value);
}

#endif  // PROJECT_BAD_HEADER_H_

Problem

The user must define <span>global_value</span> elsewhere:

// main.cc
int global_value = 42;  // Easy to overlook
#include "bad_header.h"  // Dependency on file inclusion order

6. Dependency Confusion Caused by Using .inc Files

Counterexample

// special_logic.inc (not self-contained)
void SpecialFunction() {
    std::cout << value << std::endl;  // Depends on externally defined variable
}

// main.cc (incorrect usage)
#include <iostream>

// Include .inc file without defining 'value'
#include "special_logic.inc"  // Compilation error: 'value' not declared

int value = 42;

Problem

<span>.inc</span> files do not handle their own dependencies, leading to:

  • Compilation Order Sensitivity: All dependencies must be defined before including the <span>.inc</span> file.
  • Difficult Debugging: Error messages may point to the <span>.inc</span> file rather than the actual source of the problem.

1.2 #define Guards

All header files should use <span>#define</span> guards to prevent duplicate imports. The format of the guard is: <span><project>_<path>_<filename></span><span>H</span>

To ensure the uniqueness of symbols, the name of the guard should be based on the full file path of the file in the project directory. For example, a file in the <span>foo</span> project located at <span>foo/src/bar/baz.h</span> should have the following guard:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif  // FOO_BAR_BAZ_H_

1.3. Import Your Dependencies

If a code file or header file references symbols defined elsewhere, that file should directly import (include) the header file that provides the declaration or definition of that symbol. Header files should not be imported for other reasons.

Do not rely on indirect imports. This way, when people remove unnecessary <span>#include</span> statements, it will not affect users. This rule also applies to accompanying files: if <span>foo.cc</span> uses symbols from <span>bar.h</span>, it needs to import <span>bar.h</span>, even if <span>foo.h</span> has already imported <span>bar.h</span>.

1.4. Forward Declarations

Avoid using forward declarations as much as possible. You should import the header files you need.

Definition: A forward declaration is a declaration without a corresponding definition.

// In C++ source file:
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);

Advantages:

  • Using forward declarations can save compilation time, as <span>#include</span> forces the compiler to open more files and process more input.
  • Using forward declarations can avoid unnecessary recompilation. If <span>#include</span>, changes in unrelated header files will also trigger recompilation.

Disadvantages:

  • Forward declarations hide dependencies, which may lead to overlooking necessary recompilation processes after changes in header files.
  • Compared to <span>#include</span>, the existence of forward declarations makes it difficult for automation tools to discover the module defining that symbol.
  • Modifying a library may break forward declarations. Forward declarations of functions or templates can hinder the header file owner from modifying the API, such as widening parameter types, adding default values for template parameters, migrating to new namespaces, etc., which would otherwise be harmless.
  • Providing forward declarations for symbols in the <span>std::</span> namespace can lead to undefined behavior.
  • It is difficult to determine when to use forward declarations and when to use <span>#include</span>. Replacing <span>#include</span> with forward declarations may silently change the meaning of the code:
// b.h:
struct B {};
struct D : B {};

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); }  // Calls f(B*)

If forward declarations of <span>B</span> and <span>D</span> are used instead of <span>#include</span>, <span>test()</span> will call <span>f(void*)</span>.

  • Adding forward declarations for multiple symbols is more verbose than directly using <span>#include</span>.
  • Code designed to accommodate forward declarations (e.g., using pointer members instead of object members) is slower and more complex.

Conclusion: Avoid providing forward declarations for entities defined in other projects as much as possible.

1.4 Interpretation

The core logic of Google C++ Style prohibiting forward declarations is dependency explicitness and deterministic compilation behavior. In large projects, these two principles can significantly reduce maintenance costs and collaboration complexity.

Philosophical Differences in Dependency Management

1. Google Style’s Principle of “Explicit Dependencies”

// a.h (Recommended writing style by Google)
#include "b.h"  // Explicitly depend on b.h

class A {
    B* ptr;  // Directly use B type
};
  • Advantages:
    • Self-documenting: The header file itself clearly shows all dependencies without additional documentation.
    • Tool-friendly: The compilation system and analysis tools can directly build dependency graphs through <span>#include</span>.
    • Reduces Human Error: Forcing header file inclusion avoids compilation/linking errors caused by incomplete forward declarations.

Technical Risks Caused by Forward Declarations

1. Compilation Behavior Changes Due to Incomplete Types

// Example 1: Function overload resolution error
// b.h:
struct B {};
struct D : B {};

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);

void test(D* x) { f(x); }  // Calls f(B*) // If it were a forward declaration, it might bind to f(void*)
  • Essence of the Problem: The type of a forward declaration is treated as an incomplete type, and the compiler cannot perform complete type checking.

2. Conflicts Between Templates and Forward Declarations

// Incorrect Example: Forward declaration of template
template <typename T> class vector;  // Error: Cannot forward declare template

// Correct Approach: Must include complete definition
#include <vector>
  • Reason: Templates need to be visible with complete definitions at instantiation time, which forward declarations cannot satisfy.

Considerations in Engineering Practice

1. Trade-offs in Compilation Time

Google believes:

  • **The time saved by forward declarations is overestimated: Modern compilers have significantly reduced the overhead of <span>#include</span> through precompiled headers (PCH) and parallel compilation optimizations.
  • Maintenance Costs from Over-Optimization are Higher: For example, changing member variables to pointers specifically for forward declarations increases memory indirection and management complexity.

2. Collaboration Challenges in Large Projects

  • Dependency Conflicts: In multi-team collaborations, forward declarations may lead to inconsistent dependencies on the same type across different modules.
  • Refactoring Risks: When the type being forward-declared changes, it is difficult to track which modules need recompilation.

3. Limitations of Automation Tools

  • Static Analysis Tools: Cannot perform complete data flow and type analysis based on forward declarations.
  • Code Generation Tools: Such as serialization frameworks, RPC generators, etc., rely on complete type information, and forward declarations can lead to incorrect generated code.

Why Does Google Choose a “Conservative Strategy”?

In the scale and collaboration model of Google’s code, optimizing compilation time is a lower priority than code maintainability. Prohibiting forward declarations is to avoid local optimizations that harm overall project quality, reflecting the principle of “error prevention over efficiency” in large project management.

Exceptions

Pimpl (Pointer to Implementation) Pattern

// my_class.h (Interface Layer)
class MyClass {
public:
    MyClass();
    ~MyClass();
    void doWork();
private:
    class Impl; // Forward declaration of internal implementation class (the only legal scenario)
    std::unique_ptr<Impl> impl;  // Smart pointer managing implementation
};

// my_class.cc (Implementation Layer)
#include "my_class.h"
#include "complex_dependency.h"  // Complex dependencies included only in implementation

class MyClass::Impl {
    ComplexDependency dep;  // Complex member
public:
    void doWork() { dep.process(); }
};

MyClass::MyClass() : impl(std::make_unique<Impl>()) {}
MyClass::~MyClass() = default;  // Destructor must be defined in .cc

The forward declaration in the Pimpl pattern does not violate the Google Style Guide for the following reasons:

  • Scope Limitation: The forward declaration of Pimpl (e.g., <span>struct Impl;</span>) is only used within the scope of the current class, not involving cross-module symbol exposure, encapsulating implementation details within the class.

  • Dependency Clarity: The implementation file (.cc) includes the complete header file (e.g., <span>detail_impl.h</span>), ensuring that the definition of the <span>Impl</span> struct is available at compile time, adhering to the “self-contained header file” principle.

  • No Impact on External Dependencies: The forward declaration of the <span>Impl</span> struct is not visible to external users, preventing dependency confusion and only used for internal pointer operations within the class.

Appendix: Best Practices for Pimpl Pattern under Google Style

  1. Header files should only forward declare internal structs like <span>struct Impl;</span>, without exposing any implementation details.
  2. Implementation files should include the complete header file, ensuring the definition of <span>Impl</span> is available at compile time.
  3. Avoid forward declarations of public symbols: If <span>Impl</span> needs to include classes from other projects, the corresponding header file should be included instead of forward declaring.
  4. Use unique naming to prevent conflicts: Struct names (e.g., <span>Impl</span>) should be prefixed with the class name (e.g., <span>MyClass_Impl</span>) to avoid naming conflicts.

1.5 Inline Functions

Only define small functions with fewer than 10 lines as inline.

Definition: You can suggest the compiler to expand inline functions instead of using the normal function call mechanism.

Advantages: As long as the inline function is small, it can make the target code more efficient. We encourage inline expansion for accessors, mutators, and other short functions that impact performance.

Disadvantages: Misusing inline can slow down the program. Depending on the function size, inline may increase or decrease code size. Generally, inlining very small accessor functions reduces code size, but inlining a large function significantly increases code size. On modern processors, smaller code typically executes faster due to better instruction cache utilization.

Conclusion: A reasonable rule of thumb is not to inline functions longer than 10 lines. Be cautious with destructors. Destructors are often longer than they appear because they implicitly call destructors of members and base classes! Another practical rule: inlining functions that contain loops or <span>switch</span> statements is usually counterproductive (unless these loops or <span>switch</span> statements are typically not executed). Note that even if a function is declared as inline, it may not actually be inlined; for example, virtual functions and recursive functions are typically not inlined. Generally, recursive functions should not be declared as inline. (YuleFox Note: The expansion of the recursive call stack is not as straightforward as loops, for instance, the recursion depth may be unknown at compile time, and most compilers do not support inlining recursive functions). Declaring virtual functions as inline primarily serves to define the function within the class for ease of use or to document its behavior. This is commonly used for accessors and mutators.

1.6. <span>#include</span> Paths and Order

It is recommended to import header files in the following order: accompanying header files, C language system library header files, C++ standard library header files, other library header files, and project header files.

In <span>dir/foo.cc</span> or <span>dir/foo_test.cc</span>, which implement or test the content of <span>dir2/foo2.h</span>, header files should be imported in the following order:

  1. <span>dir2/foo2.h</span>.

  2. Empty line

  3. C language system files (specifically: header files using angle brackets and <span>.h</span> extension), such as <span><unistd.h></span> and <span><stdlib.h></span>.

  4. Empty line

  5. C++ standard library header files (without extensions), such as <span><algorithm></span> and <span><cstddef></span>.

  6. Empty line

  7. Other library <span>.h</span> files.

  8. Empty line

  9. Project <span>.h</span> files.

Each non-empty group should be separated by an empty line. This order ensures that when <span>dir2/foo2.h</span> lacks necessary imports, building <span>dir/foo.cc</span> or <span>dir/foo_test.cc</span> will fail. This way, the maintainers of these files will be the first to discover build failures, rather than innocent maintainers of other libraries.<span>dir/foo.cc</span> and <span>dir2/foo2.h</span> are usually located in the same directory (e.g., <span>base/basictypes_unittest.cc</span> and <span>base/basictypes.h</span>), but sometimes they are placed in different directories. Note that C language header files (such as <span>stddef.h</span>) and their corresponding C++ header files (<span>cstddef</span>) are equivalent. Both styles are acceptable, but it is best to remain consistent with existing code. Import statements within each group should be arranged in alphabetical order. Note that old code may not adhere to this rule and should be corrected when convenient. For example, the import statements in <span>google-awesome-project/src/foo/internal/fooserver.cc</span> are as follows:

#include "foo/server/fooserver.h"

#include <sys/types.h>
#include <unistd.h>

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "foo/server/bar.h"
#include "third_party/absl/flags/flag.h"

Exceptions

Sometimes platform-specific code needs to be conditionally imported, in which case conditional import statements can be placed after other import statements. Of course, try to keep platform-specific code concise and with a small impact. For example:

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

Leave a Comment