C++ Readabilities

In this page, I will introduce some best practice recommended by Google. We should write readable, efficient, and extendable codes.

Use EXPECT_THAT & ElementsAre & UnorderedElementsAre

It can be more flexible than EXPECT_EQ.

EXPECT_THAT(Foo(), ElementsAre(1, Gt(0), _, 10));

If no order is required, consider UnorderedElementsAre.

Test Suite Names and Test Names not Contain Underscore

https://github.com/google/googletest/blob/master/googletest/docs/faq.md#why-should-test-suite-names-and-test-names-not-contain-underscore

Prefer Range-Based Loops

If possible, use for (const auto i& : v) rather than index-based loops for better performance (same reason as avoiding vector<T>& below).

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Res-for-range

Avoid vector<T>& or string &

When using a pointer (or reference) to a container, it usually contains two levels of pointer arithmetic. Consider using absl::Span or absl::string_view instead.

void Foo(vector<T> &a) {
  a[0] = 1; // actually it is a->data()[0] = 1
}

// method 1
void Foo(absl::Span<T> a) {
  a[0] = 1; // data[0]=1, only one level of pointer arithmetic
}

// method 2
void Foo(vector<T> &a) {
  absl::Span<T> b(a);
  for (...) {
    b[i] = 1;
  }
}

Two levels of pointer arithmetic makes it really hard for compilers to do analysis. When read or write data in loops, absl::Span becomes much faster because of vectorization.

Avoid [] parameters, prefer span

Use absl::Span instead for better readability and safety.

Avoid Calling new & delete Explicitly

Suggest using make_unique instead.

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#r11-avoid-calling-new-and-delete-explicitly

StrCat

Avoid foo + bar + baz, use absl::StrCat(foo, bar, baz) instead. Since for chains of n string concatenations, in the worst case, it requires O(n) reallocations.

 https://abseil.io/tips/3

Order of Includes

Separate headers by sections. Within each section the includes should be ordered alphabetically. No UNIX directory aliases . (the current directory) or .. (the parent directory) please.

  1. dir2/foo2.h (the declaration if applicable)
  2. A blank line
  3. C system headers (more precisely: headers in angle brackets with the .h extension), e.g., <unistd.h><stdlib.h>.
  4. A blank line
  5. C++ standard library headers (without file extension), e.g., <algorithm><cstddef>.
  6. A blank line
  7. Other libraries’ .h files.
  8. Your project’s .h files

As an example, in foo/server/fooserver.cc

#include "foo/server/fooserver.h"

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

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/server/bar.h"

Google’s C++ Stype Guide#Names_and_Order_of_Includes

Namespace

  1. Do not use using-directive (using namespace foo;)
  2. With few exceptions, place code in a namespace (include using-declarations, like using foo::bar;).
  3. Fully qualify using-declarations and namespace aliases; in other cases, please try to avoid fully qualifying unless you must to make it compile;
  4. Don’t introduce sub-namespaces with common names like stdutil, or testing.
namespace foo {
using ::foo1::bar::Baz;
namespace baz = ::foo2::bar::baz;
}

https://abseil.io/tips/153, https://abseil.io/tips/130

Namespace Lookup

namespace foo {
namespace bar {

void Func() {
  Baz::HaHa b;
}

}  // namespace bar
}  // namespace foo

In C++, lookup on an unqualified name (Baz::HaHa) will search expanding scopes for a symbol of the same name: first in Func (the function), then in bar, then in foo, then in the global namespace.

If Baz refers to a global namespace and in bar there is another namespace called Baz, but without HaHa defined, then building will fail, since namespace lookup looked into ::foo::bar::Baz first, failed, and stopped. Use fully qualified ::Baz::HaHa will solve the problem.

https://abseil.io/tips/130

Reduce Double-Negatives

// bad
if (!disable_b) {
  DoA();
} else {
  DoB();
}

// good
if (enable_b) {
  DoB();
} else {
  DoA();
}

Reduce Nesting Levels

Level Depth is better to be no more than 2, which could be achieved by early return or separate functions.

Function Comments

Declaration comments describe use of the function (comments should use the indicative mood like “Returns the area”, rather than the imperative “Return the area”; definition comments describe implementation details.

string_view

Prefer string_view over const char* or const string& as function parameters.

Note thatstring_view is not necessarily NUL-terminated.

https://abseil.io/tips/1

Avoid Boolean Parameters

Prefer enum over boolean parameters. Foo(Feature::kA) makes more sense than Foo(/*enable_A*/true).

https://abseil.io/tips/94

Output Parameters

Prefer return value over output parameters (please refer to StatusOr in protobuf, the StatusOr on Abseil is not open sourced yet).

Also output parameters should be after input parameters.

Prefer span as Function Parameters

Same as string_view over const char* or const string&, prefer span<const T> over const T* or const vector<T>& as function parameters.

Usually pass span or string_view by value, since it is not always faster to pass a pointer. See Copy Elision and Pass-by-value in https://abseil.io/tips/117.

https://abseil.io/tips/93

Qualifying auto

  1. Use const auto& or auto& unless a copy is needed;
  2. Use const auto& unless you need to modify data;
  3. Use const auto* or auto* to make pointer explicit;
char num = 'a';
char* p1 = #
auto p2 = p1;   // p2 is 'char*'
auto* p3 = p1;  // p3 is 'char*' (same as bare auto)

Reserve Size in Advance

Call reserve if the final size is known to avoid repeated allocation and the corresponding copy/moves.

Since if a reallocation happens, old values might be copied into the new memory.

Prefer push_back over emplace_back

Prefer push_back over emplace_back unless the performance matters. Since emplace_back is too powerful.

std::vector<vector<int>> vec;
vec.push_back(1<<20); // compile error
vec.emplace_back(1<<20); // no errors until run-time

https://abseil.io/tips/112, https://abseil.io/tips/65

Global Constants

It is very common that people want to define static const variables in the global namespace.

  1. One global variable shouldn’t rely on another global in a different translation unit (roughly, a cc file), since the order of initialization between translation units is unspecified.
  2. Ensure the One Definition Rule (external linkage rather than internal linkage). This is important when address matters.
  3. Global constants should be trivially destructible (string is not). As a rule of thumb: a global variable satisfies these requirements if its declaration, considered in isolation, could be constexpr.

Constants in Header

Before C++17, use extern const variables.

// constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
 
namespace constants
{
    // since the actual variables are inside a namespace, the forward declarations need to be inside a namespace as well
    extern const double pi;
    extern const double avogadro;
    extern const double my_gravity;
}
 
#endif

// constants.cc
#include "constants.h"
 
namespace constants
{
    // actual global variables
    extern const double pi { 3.14159 };
    extern const double avogadro { 6.0221413e23 };
    extern const double my_gravity { 9.2 }; // m/s^2 -- gravity is light on this planet
}

After C++17, use inline variables.

#ifndef CONSTANTS_H
#define CONSTANTS_H
 
// define your own namespace to hold constants
namespace constants
{
    inline constexpr double pi { 3.14159 }; // note: now inline constexpr
    inline constexpr double avogadro { 6.0221413e23 };
    inline constexpr double my_gravity { 9.2 }; // m/s^2 -- gravity is light on this planet
    // ... other related constants
}
#endif

Constants in Source

constexpr int kNum = 42;
constexpr char kStr[] = "hello";
constexpr std::string_view kAnotherStr = "world";

https://www.learncpp.com/cpp-tutorial/global-constants-and-inline-variables/

https://google.github.io/styleguide/cppguide.html#Static_and_Global_Variables

Pass Primitives & Enums by Value

Pass primitives and enumerators (including enum class) by value, since usually primitives are smaller than pointers. Passing by pointers or references also makes codes harder to be optimized by compiler.