Skip to content

Syntax and Semantics

Declarations vs. Definitions, Header Files

Because of limitations of ancient C/C++ compilers, the language separated declarations of functions/types from their definitions. During compilation you only need the declarations (signatures) everywhere, and then at link time all the generated code is put together. Today (before C++ Modules are universally adopted) we still follow this: for any function/type that is not confined to the current translation unit, you first declare it, then use it.

In C++ the convention is:

  • Use files ending with .h to hold declarations.
  • Use files ending with .cc / .cpp to hold implementations.
  • Use .inc for textual fragments that are meant to be #included (data tables, code snippets, etc.).

Note

If a function or class template is used, its implementation must live in the header file (so that the compiler can instantiate it when needed).

constexpr int64_t kAnswer = 42;

void SayHello();

class Example {
 public:
  Example();
  virtual ~Example();

  int64_t IncThenGet();

 private:
  int64_t data_{kAnswer};
};

// The implementation of this method must be defined in the header file.
template <typename T>
void ignore_result(const T&) {}

// example.cc
void SayHello() { LOG(INFO) << "Hello!"; }

Example::Example() { LOG(INFO) << "Answer = " << data_; }

Example::~Example() { LOG(INFO) << "Answer = " << data_; }

int64_t Example::IncThenGet() { return ++data_; }

Package and Namespace

C++ uses namespaces to achieve an effect similar to Java packages. There is no package-visible access level in C++.

Read-Only Qualifiers

Currently C++ has these related specifiers:

  1. const
  2. constexpr
  3. consteval (C++20)
  4. constinit (C++20)

The const Specifier

const has two meanings:

  1. Produce a read-only variant of a type (e.g. from int32_t to const int32_t).
  2. Declare something as a constant object.

For meaning (1) see: Programming Language -> Type System -> Read-only Types. For meaning (2) see: Programming Language -> Type System -> Constants.

The constexpr, consteval, and constinit Specifiers

Because the two meanings of const can be ambiguous, C++ introduced constexpr to separate (2). Recommendation: always use const for (1) and constexpr for (2).

However, constexpr itself has two uses:

  1. Declare a constant object.
  2. Declare that a function may be evaluated at compile time (loosely speaking; see the precise rules at https://en.cppreference.com/w/cpp/language/constexpr).

Use case (2) was further split out into consteval.

Use case (1) still has deeper subtleties. Earlier we mentioned: do not declare a std::string constant (but declaring a char* constant is fine). There are deeper issues here: can a std::string be created at compile time? If not, how do we create a constant of type std::string? Are there destructor constraints? To address initialization of non-trivial types as constants, C++20 added constinit. See https://en.cppreference.com/w/cpp/language/constinit for details.

Virtual Functions: final and override

In Java you can use final to prevent a class from being subclassed or a method from being overridden. C++ uses the same final keyword.

Java uses the @Override annotation to mark an overriding method. C++ goes further and adds the override specifier.

struct Base {
  virtual void foo();
};

struct A : Base {
  void foo() final;  // Base::foo is overridden and A::foo is the final override
  void bar() final;  // Error: bar cannot be final as it is non-virtual
};

struct B final : A {    // struct B is final
  void foo() override;  // Error: foo cannot be overridden as it is final in A
};

struct C : B {  // Error: B is final
};

Interface Types and Pure Virtual Functions

C++ has no separate "interface" construct. An abstract class with no data members can conceptually serve as an interface. Since C++ permits multiple inheritance, this usage is workable. A pure virtual function is declared like this:

class Base {
 public:
  virtual void Say() = 0;
};

Note

When you design interface-like or abstract base types (similar to Java style), you normally follow "reference semantics" guidelines:

  1. Disable copying.
  2. Provide a virtual destructor.
  3. (Preferably) allow move.

Pass by Value, Pointer, Reference

In Java, primitives are passed by value and other types behave like references. In C++ it depends on the semantic design of each type; see: Programming Language -> Type System -> Custom Types -> (copy-by) value semantic types / reference semantic types.

Except where a pointer is explicitly required by convention (e.g., to indicate optional ownership transfer or C APIs), avoid raw pointers. To represent an optional value prefer std::optional; see Programming Language -> Type System -> std::optional.

RAII, Ownership, and Move Semantics

RAII (Resource Acquisition Is Initialization) is analogous to Java's try-with-resources. You control a resource via an object's lifetime. In Java you might manage a file resource like this:

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
                   new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

When the try block ends, BufferedReader.close() is invoked automatically to release the OS file handle. Similarly, in C++ you can perform the cleanup inside the destructor:

class UniqueFileDescriptor {
 public:
  static constexpr int kInvalidFileDescriptor = -1;  // From man 2 open

  UniqueFileDescriptor() = default;
  explicit UniqueFileDescriptor(int fd) : fd_(fd) {}
  ~UniqueFileDescriptor() {
    if (fd_ != UniqueFileDescriptor::kInvalidFileDescriptor) {
      ::close(fd_);  // Should LOG error if failed to close it.
    }
  }

  // omitted: copy constructor and copy assignment operator should be deleted.
  // omitted: move constructor and move assignment operator should be defined.
  // omitted: other utility methods.

 private:
  int fd_{UniqueFileDescriptor::kInvalidFileDescriptor};
};

In C++ we often allocate small objects on the stack; their lifetime is bound to scope. Sometimes we need to transfer the managed resource out of the current scope to another owner. In that case we need move semantics to "transfer" the internals of an object to another.

UniqueFileDescriptor unique_fd;
{
  UniqueFileDescriptor fd(::open(filename.c_str(), O_RDONLY));
  if (fd.fd() == UniqueFileDescriptor::kInvalidFileDescriptor) {
    return absl::UnknownError("Failed to open file.");
  }

  /* do something ... */

  // Move the control to outer |unique_fd|.
  unique_fd = std::move(fd);
}

Operator Overloading

C++ allows operator overloading. Common use cases:

  1. Overload copy/assignment operator (see Programming Language -> Type System -> Custom Types -> (copy-by) value semantic / reference semantic types).
  2. Overload comparison operators for natural ordering of custom data types.
  3. Overload the function call operator () for functors (so an instance can be invoked like a function).
  4. Overload [] for custom containers.
  5. Overload << for logging / streaming output.

Note

Avoid assigning bizarre semantics to operators that change the normal syntactic expectations of C++. Although this can build clever DSLs, teammates unfamiliar with the background can easily misuse them. If it is not too complex, prefer plain functions to build DSLs—they make intent clear.

Two styles exist for overloading operators: member functions and non-member (often friend) functions:

class Point {
 public:
  bool operator<(const Point&) const;  // Declare a member operator overload.

  // Declare addition operators.
  friend Point operator+(Point&, int);
  friend Point operator+(int, Point&);
};

// Declare a global operator overload.
bool operator==(const Point& lhs, const Point& rhs);

If you need access to private members, you usually must use a friend function.

Equality Comparable and Hash Function

inline bool operator==(const X& lhs, const X& rhs) { /* do actual comparison */
}
inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }

Often after defining equality you also want to use the type as a key in hash-based containers. Provide a hash function; leveraging Abseil's Hash library is recommended:

class Circle {
 public:
  // ...

  template <typename H>
  friend H AbslHashValue(H h, const Circle& c) {
    return H::combine(std::move(h), c.center_, c.radius_);
  }

  // ...

 private:
  std::pair<int, int> center_;
  int radius_;
};

// Use it in unordered_map as Key type.
std::unordered_map<Circle, MyValue, absl::Hash<Circle>> my_map;

Strict Weak Ordering (Partial Order)

inline bool operator<(const X& lhs, const X& rhs) { /* do actual comparison */ }
inline bool operator>(const X& lhs, const X& rhs) { return rhs < lhs; }
inline bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); }
inline bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }

Total Ordering

A total order must satisfy the constraints listed at: https://en.cppreference.com/w/cpp/concepts/totally_ordered

A common pattern: first implement a compareTo()-like function, then implement the operators in terms of it:

inline bool operator==(const X& lhs, const X& rhs) {
  return cmp(lhs, rhs) == 0;
}
inline bool operator!=(const X& lhs, const X& rhs) {
  return cmp(lhs, rhs) != 0;
}
inline bool operator<(const X& lhs, const X& rhs) { return cmp(lhs, rhs) < 0; }
inline bool operator>(const X& lhs, const X& rhs) { return cmp(lhs, rhs) > 0; }
inline bool operator<=(const X& lhs, const X& rhs) {
  return cmp(lhs, rhs) <= 0;
}
inline bool operator>=(const X& lhs, const X& rhs) {
  return cmp(lhs, rhs) >= 0;
}

With C++20 you can use the new three-way comparison operator <=>:

struct Record {
  std::string name;
  unsigned int floor;
  double weight;
  auto operator<=>(const Record&) const = default;
};
// records can now be compared with ==, !=, <, <=, >, and >=

Lambda Expressions

C++ lambdas are similar to Java's but require you to explicitly list which variables to capture and how (by copy, by reference, or other special forms). A blanket capture of everything ([&] or [=]) is generally discouraged.

int a;
int b;
std::unique_ptr<int> c;
int d;

// Take care that the lambda need to be called while |b| is still alive.
auto lambda = [a /* copy a */, &b /* reference b */,
               c = std::move(c) /* calculated result */](
                  /* parameters */) {
  // you cannot use |d| because you didn't capture it.
  // ensure the reference captured variables are still living.
};

(Emphasized) Free Functions in C++

C++ functions need not live inside a class. So do not write Java-like utility classes:

final class MyUtil {
  private MyUtil() {}

  static void SayHello() { System.out.println("Hello!"); }
}
class MyUtil final {
 public:
  MyUtil() = delete;

  static void SayHello();
};

Just write the free function directly:

void SayHello();

Comments