C++ is a statically-typed, low-level programming language that supports object-oriented programming. It’s frequently used in any software system that requires resource efficiency such as operating systems, game engines, databases, compilers, etc.
C/C++‘s high performance is attributed to how closely it’s constructs and operations match the hardware.
The ISO C++ standard defines:
- Core language features — data types, loops, etc.
- Standard library components —
vector
,map
,string
, etc.
Also see C++ standard library.
Core
Copy, List and Direct Initialisation
There are a few ways to initialise a variable with a value.
- Copy initialisation: using
=
. It implicitly calls a constructor. - List initialisation, also called uniform initialisation: using
{ }
. - Direct initialisation: using
( )
. Think of the parentheses as being used to directly invoke a specific constructor.
Prefer uniform initialisation over copy initialisation.
```cpp
int b(1); // Direct initialisation.
int a{1}; // List initialisation.
int c = 1; // Copy initialisation.
int d = {1}; // Copy/List initialisation.
```
- List initialisation does not allow narrowing. Try to use list initialisation
{ }
more often. explicit
constructors are not invokable with copy initialisation.
Designated Initialisers
C++20 introduces a new way to initialise the members of a class/struct:
Pointers and References
Pointers and references are really the same thing under the hood, however they have different semantics to the programmer. You can consider references as syntactic sugar for pointers whose main purpose is to help you write cleaner code, compared to if you were to use pointers for the same use case.
Unlike other languages, in C++, arguments are always passed by value by default unless the function signature explicitly says it takes in a pointer or reference. This means functions will entirely copy all the objects you pass in, unless you pass in a pointer/reference.
*
and&
have different meanings depending on whether they appear in a type declaration (LHS) or whether they appear in an expression that is to be evaluated (RHS).
In a type declaration:
*
defines a pointer type.int* arr;
&
defines a reference variable.
In an expression:
*
is the unary dereference operator that dereferences an address to evaluate to the contents at that address.&
is the unary address-of operator that evaluates to the address of a variable.&
always expects an lvalue.
Pointers
Pointers are just memory addresses, often to the contents of an object allocated on the heap.
nullptr
. C++ requires thatNULL
is a constant that has value0
. Unlike in C,NULL
cannot be defined as(void *)
nullptr
therefore exists to distinguish between 0 and an actual null for pointer types. People would otherwise mistakenly useNULL
and not realise it is just 0
- Note: Stroustrup prefers the pointer declaration style
int* p
in C++ andint *p
in C.
References
You can think of a reference variable as an alias for another variable. They don’t occupy any memory themselves, once your program is compiled and running.
- References are useful as function parameters to avoid copying the entire argument. In C++, pass-by-value is the default, although copy elision can happen which nullifies the performance impact of making a copy of an object.
- Const references are useful for when you don’t want to modify an argument and just want to read its contents. It prevents the need to make a copy of that argument for the function’s scope. This is really common practice:
- References must be initialised and can’t be reassigned afterwards.
- When you return a reference, you are ‘granting the caller access to something that isn’t local to the function’. It is an error to return a reference to a local variable.
new
new
is used to instantiate classes and arrays on the heap. All objects allocated with new
must have a corresponding delete
somewhere.
- You should always prefer stack allocations rather than heap allocations.
- This helps avoid memory leaks because when the variable is allocated for in the stack, its destructor is automatically called when leaving its scope
- It is generally better to avoid having to use
new
anddelete
in your code, or at least the code that’s using your code shouldn’t have to be responsible for memory management. - Prefer
new
anddelete
over C-stylemalloc
andfree
. The reasons are: type safety (sincenew
ensures instantiated objects are initialised), readability,new
throws an exception on failure whilemalloc
doesn’t.
delete
There are two delete operators, delete
and delete[]
.
delete
— for individual objects. It calls the destructor of that single object.delete[]
— for arrays. It calls the destructor on each object.
Type Qualifiers: auto, const, constexpr, static
Auto
When specifying the data type of something as auto
, C++ automatically infers the type.
- Use
auto
for concision, especially when long generic types are involved. - It’s fine to use copy initialisation if you use
auto
since type narrowing won’t be a problem. E.g.auto x = 1
. - Always assume that
auto
, by itself, will make a copy of the RHS. Useauto&
if copying is undesirable (such as when copying large vectors).
Const
The const
qualifier makes it ‘impossible’ to assign a new value to a variable after it’s initialised. There is 0 negative performance impact of enforcing const
since it’s all done at compile-time. Using const can actually allow the compiler to make optimisations.
Prefer making things const by default. See const correctness for a pitch on why.
const
andconstexpr
— immutable variables. Declaring and initialising aconst
variable will make the compiler guarantee that its value is never modified, ever.
Const Pointers
If this is hard to read, see the clockwise-spiral rule.
Const References
Typing a variable as a const reference makes it a read-only alias. It’s especially helpful for function parameters.
Prefer typing function parameters as const references. This gives the caller confidence that what they pass in is not modified in any way.
If you don’t want a function to modify a caller’s argument, you have these options:
You can’t have 100% certainty that what you pass as a const reference is unchanged. See this example from isocpp:
Const Methods
Const methods can only read this
and never mutate any of its members. The this
pointer will essentially become a pointer to const
object. To specify a const method, the const
qualifier must be placed after the parameter list.
Methods that don’t modify object state should be declared
const
. See this const-correctness article
What about making methods return const values, eg.
const Foo bar();
? It’s mostly pointless. However, it is not pointless if you’re returning a pointer or reference to something that is const.
Mutable
Declaring a method with const
will cause a compiler error to be raised for when that method attempts to change a class variable.
You can add the mutable
keyword to allow an exception for what class variables can be modified by const member functions.
Constexpr
The constexpr
type specifier is like const
, except the RHS value must be able to be determined at compile-time.
Constexpr Functions
Constexpr functions are those than can be executed at compile-time, meaning all its state and behaviour is determinable at compile-time.
Static
Static Variables
Inside functions, static variables let you share a value across all calls to that function.
Static function variables are generally considered bad because they represent global state and are therefore much more difficult to reason about*.
Clockwise-Spiral Rule
Clockwise-Spiral Rule is a trick for reading variable types.
- Start at the variable name.
- Follow an outwards clockwise spiral from that variable name to build up a sentence.
Example:
Starting at the name fp
:
fp
is a pointer.fp
is a pointer to a function (that takes in anint
and afloat
pointer).fp
is a pointer to a function (that takes in anint
and afloat
pointer) that returns a pointer.fp
is a pointer to a function (that takes in anint
and afloat
pointer) that returns a pointer to a char.
More examples:
IO
<<
— the ‘put to’ operator. In arg1 << arg2
, the <<
operator takes the second argument and writes it into the first.
>>
— the ‘get from’ operator. In arg1 >> arg2
, the >>
operator gets a value from arg1
and assigns it to arg2
.
std::endl
is a newline that flushes the output buffer, which means it is less performant than "\n"
.
See C++ Standard Library IO for more complex IO operations.
Arrays
The many ways of initialising arrays:
The size of the array must be able to be determined at compile-time.
Pointers vs. Arrays
What’s the difference between int* array
and int array[]
? They both can be used to access a sequence of data and are mostly interchangeable.
The main difference is in runtime allocation and resizing: int* array
is far more flexible, allowing allocation/deallocation and resizing during runtime, whereas int array[]
cannot be resized after declaration.
In general, prefer using declaring true array-types with
[]
over pointers-type arrays with*
. It’s less error-prone (because you don’t have to worry about dynamic allocation and remembering to free allocated memory) and more readable.
L-Values and R-Values
An lvalue is a memory location that identifies an object. Variables are lvalues.
In C: an lvalue is an expression that can appear on the LHS or RHS of an assignment.
An rvalue is a value stored at some memory address. Rvalues are different from lvalues in that they cannot have a value assigned to it, which means it can’t ever be on the LHS part of an assignment. Literals are typically rvalues.
In C: an rvalue is an expression that can only appear on the RHS of an assignment.
- Rvalues are important because they enable move semantics in C++. There are many instances in C++ code where it’s not necessary to copy a value or object from one place to another. E.g. when passing arguments into a function or when saving the returned value on the caller’s side. Implementing move semantics, where appropriate, is great for performance because it prevents expensive copies.
Very loosely, Bjarne describes lvalues as “something that can appear on the left-hand side of an assignment” and rvalues as “a value that you can’t assign to.”
L-Value Reference
An lvalue reference uses a single ampersand &
, eg. string& s = "..."
- Const lvalue reference types (eg.
const string& s
) as a function parameter allow the caller to pass both an l-value or r-value, equivalently.
R-Value Reference
An rvalue reference uses double ampersand &&
, eg. string&& s
. You’d use this to receive rvalues in functions, like literals and temporary objects. Doing this means you can avoid unnecessarily copying a value that is a ‘throwaway’ on the caller’s side.
- You can define a move constructor and move assignment operator that take in an rvalue reference instead of a const lvalue reference. It’ll behave the same way, but it won’t guarantee the source to be unchanged.
Modularity
Separate Compilation
C++ supports separate compilation, where code in one file only sees the declarations for the types and functions it uses, not the implementation. This decouples the smaller units comprising a project and minimises compilation time since each unit can be compiled only if they change.
We take advantage of separate compilation by listing out declarations in a header file. Example:
Namespaces
Namespaces define a scope for a set of names. It’s used to organise your project into logical groups and to prevent name collisions when you’re using libraries, for example.
- You can do namespace aliases to shorten namespaces. Never do this in header files
- Use
using
to avoid using a fully qualified name every time. - Any identifier you declare that’s not within a namespace will be implicitly part of the global namespace. Globally scoped identifiers are accessible with
::
without specifying a name.
Error Handling
C++ provides the familiar try
and catch
blocks for error handling. Note that when an exception is thrown, the destructor for the object that threw the exception is called, enabling RAII.
Custom Exceptions
Just inherit from std::exception
, implement the const char* what() const throw()
method, and a constructor that takes in a string
error message.
noexcept
Use noexcept
at the end of a function signature to declare that it will never throw an exception. If it does in fact throw an exception, it will just directly std::terminate()
.
Why use it?
- The compiler generates slightly more optimal code since it can assume it doesn’t have to support try-catch control flow.
- For documentation for other developers.
Classes
- Why do non-const static members have to be initialised outside the class? See this explanation.
Inheritance
Inheritance is done with the syntax class Foo : public Bar
. Also see Protected and Private Inheritance.
- Constructors are not inherited by default.
- Child constructors can reference the parent constructors in the member initialiser list following the constructor signature.
- Multiple inheritance is done like this:
class Foo : public Bar, public Baz
.
Member Initialiser List and Delegating Constructors
Use a member initialiser list following a constructor signature to initialise class variables and invoke other constructors.
- ⚠️ This is not to be confused with list initialisation.
- This is not just a shorthand:
- You must initialise const members in the member initialiser list.
- Initialisation is always done before the executing the constructor body.
- Order matters. Initialise members in the same order that they’re declared in the class as a good practice.
Virtual Methods
Virtual methods are methods that have an implementation but which may be redefined later by a child class.
- Pure virtual method — where a function must be defined by a class deriving from this one
```cpp
class Foo {
public:
virtual void Bar() = 0;
}
- ***Abstract class*** — a class that has at least 1 *pure virtual method*. It cannot be instantiated - C++ doesn't have an `abstract` keyword like Java. To make a class abstract, you just define 1 pure virtual method
- Concrete class — a class that has no pure virtual functions and can be directly instantiated
override
keyword — is an optional qualifier that tells programmers that a method is meant to provide a definition for a virtual method from a base class- Any class with virtual functions should always provide a virtual destructor
Object Slicing
When you copy a child object to a variable of the type of the parent, the members specific to the child are ‘sliced off’ so that the resulting object is a valid instance of the parent type.
- An important consequence of object slicing is that you can no longer call the child class’ overridden methods on when the variable is of the parent type. In the following example, the output is “Something”:
- If you used
Parent* foo = new Child();
instead, the output would be “Overridden”.
- If you used
Object slicing does not happen in the same way in most other languages, like Java. When you do
Parent foo = new Child()
in Java,foo
’s overriden methods are still intact and will be called instead of the base method. This is because languages like Java use implicit references to manipulate objects and objects are copied by reference by default, unlike C++. (sourced from GeeksForGeeks)
Instantiating Classes
There are several ways of instantiating a class:
Note that when using brace initialisation, the constructor that takes in a std::initializer_list
will be preferred for invocation.
General Guidelines*:
- Use
=
if the (single) value you are initialising with is intended to be the exact value of the object.- Prefer using
=
when assigning toauto
variables. - Prefer when initialising variables with primitive types (eg. int, bool, float, etc.).
- Prefer using
- Use
{ }
if the values you are initialising with are a list of values to be stored in the object (like the elements of a vector/array, or real/imaginary part of a complex number).- Prefer using { } in the majority of cases because it can be used in every context and is less error-prone than the alternatives.
- Use
( )
if the values you are initialising with are not values to be stored, but describe the intended value/state of the object, use parentheses.- Essentially, if the intent is to call a particular constructor, then use parentheses
( )
. - E.g. good example with
vector
:
- Essentially, if the intent is to call a particular constructor, then use parentheses
- In general, prefer allocating on the stack rather than the heap, unless you need the object to persist after the function terminates.
- You’d have to use
unique_ptr
if you don’t want to memory-manage withnew
anddelete
.- Allocating on the heap is less performant than allocating on the stack. Stack allocation is faster because the access pattern makes it trivial to allocate and deallocate memory from it (a pointer/integer is simply incremented or decremented), while the heap has much more complex bookkeeping involved in memory allocation/deallocation.
- Very large objects should be allocated on the heap to prevent stack overflow (the heap is larger than the stack).
- You’d have to use
- Note: In Java/C#, you can’t allocate objects on the stack, they’d all be allocated on the heap. You could use a
struct
instead
Const Objects
const
objects prevents its fields being modified. You can only call its const methods.
Note the differences between
const Foo foo = ...;
in C++ vs.const foo = ...
in JavaScript/TypeScript. In JavaScript, you can still mutate the fields freely by default, you just can’t reassignfoo
to point to a different object.
Final Methods
Postfixing a method signature with the final
keyword will make it so that it cannot be overridden by a deriving class.
- Why not just declare a method without the
virtual
modifier? Wouldn’t that prevent overriding? See why final exists.
Explicit Methods
You can prefix explicit
in front of a constructor or method signature to prevent implicit type conversions.
- It’s good practice to make constructors
explicit
by default, unless an implicit conversion makes sense semantically. Source.- Why doesn’t the language make constructors explicit by default? Because people thought it was cleaner and now it’s irreversible because people want backward compatibility (source).
Friend Classes
Inside your class body, you can declare a class as a friend
which grants them full access to all your class’ members, including private ones. Just remember the inappropriate but memorable phrase: “friends can access your privates”.
A class can declare who their friends are in their body. Friends are then able to access everything within that class, including private members.
- You can only declare who’s allowed to access you, not who you can have access to. Like in real life, you can’t grant yourself access to someone else’s privates, but you can grant others access to yours 😏.
- You can declare other classes or specific functions as your friends.
When to use friend classes?
- When you want to write white-box unit tests, you can declare the unit test class as a friend. It’s good for unit testing private methods which you don’t want to expose to anyone other than the unit testing class.
- Caveat: it’s debatable whether it’s good practice to test private methods. Testing a public method should indirectly test a private method anyway.
Use friend classes sparingly. If you’re in a situation where you want to use a friend class, it’s a red flag, design-wise.
Deleted Functions
Just like how you can use = 0
to declare a function to be a pure virtual function, you can use = delete
to declare a function to be a deleted function, where it can no longer be invoked. This is mostly used to suppress operations and constructors to prevent misuse.
Defaulted Functions
Like deleted functions, you can postfix a constructor or method signature with = default
to make the compiler supply a default implementation.
RAII
The technique of acquiring resources in the constructor and then freeing them in the destructor is called RAII (Resource Acquisition is Initialisation). The idea is about coupling the use of a resource to the lifetime of an object so that when it goes out of scope, or when it throws an exception, the resources it held are guaranteed to be released. Always design classes with RAII in mind.
- This works well for mutexes where you can acquire the lock in the constructor and unlock it in the destructor.
- RAII is implemented religiously in standard library resources such as threads, files, data structures, etc.
Operator Overloading
You can define operations on classes by overloading operators like +
, +=
, ==
, etc.
- Operator overloading is just a type of static polymorphism. Operators are just functions. When compiled, expressions with operators are just converted to equivalent function calls. E.g.
a += b
becomesoperator+=(a, b)
. - Operator overloading also exists in C#, Java, Python, etc.
- Make sure the copy constructor/assignment is implemented such that logical equivalences hold. E.g.
a != b
should be the same as!(a == b)
, etc.
Copy Constructor and Operation
A copy constructor is a constructor that takes in a (typically const) reference to an instance of the same class. A copy assignment operator overload also takes in a (typically const) reference to an instance of the same class and returns a reference to an instance of the same class.
The copy constructor is invoked implicitly in a number of situations:
- Passing by value to a function (the function creates its own copy for local use).
- Returning a value will create a copy for the caller (this can be very inefficient, which is why optimisations like copy elision and move semantics exists).
- Copy initialisation:
If you don’t implement the copy constructor yourself, the compiler generates a default copy constructor that performs a simple memberwise copy to form a new object. Often, this default copy constructor is acceptable. For sophisticated concrete types and abstract types, the default implementation should be deleted or manually implemented.
-
An example of when default copy could be bad: when your class holds a pointer to a resource, a memberwise copy would copy the pointer over to the new object. Now both objects would affect the same resource.
E.g. suppose we have a crappy
Vector
implementation that uses the default copy constructor and copy assignment operator.Vector
manages a pointer to an underlying array. When you just do a memberwise copy, you only copy over the pointer to the underlying array, not the underlying array itself.
Move Constructor and Operation
A move constructor is a constructor that takes in an rvalue reference to an instance of the same class. A move assignment operator overload takes in an rvalue reference (to an instance of the same class) and returns a reference to the same class.
Suppose you have a function that returns a large object (e.g. a big matrix) and you want to avoid copying it to the caller, which would be very wasteful of clock cycles. Since you can’t return a reference to a local variable, and it is a bad idea to resort to the C-style returning of a pointer to a new
object that the caller has to memory-manage, the best option is to use a move constructor.
- Move constructors are implemented to ‘steal’ over the resources held by the given instance (hence why we don’t take in a
const
rvalue reference). This means transferring over things like underlying data structures, file handles, etc.
std::move
Suppose you have a class Foo
that implements a move constructor: Foo(Foo&& other)
. To invoke this, you’d do something like:
Since this is ugly and doesn’t work for auto
inferred types, we have std::move
instead.
Once you’ve used move(foo)
, you mustn’t use foo
again. Because it’s error prone, Bjarne recommends to use it sparingly and only when the performance improvements justify it.
std::move
doesn’t actually move anything, which is a bit misleading. It just converts an lvalue to rvalue reference so as to invoke the move constructor. It has zero side effects and behaves very much like a typecast. The actual ‘moving’ itself is done by the move constructor.A less mislead name for
std::move
would have beenrvalue_cast
, according to Bjarne.
std::move(foo)
basically says “you are now allowed to steal resources fromfoo
”.
Templates
Templates in C++ are a code generation system that lets you specify a general implementation of a class or function and then generate specialised instances of that template for different types, on demand.
- Templates provides everything offered by generics in languages like Java, plus more.
- Since templates are purely a compile-time feature, there is no real difference between template-generated code and equivalent handwritten code.
To make a class or function a template, just prefix the definition with the template
keyword followed by a list of type parameters: template <typename A, typename B, ...>
.
- Read
template <typename T>
as “for all types T, …” - You can also use the
class
keyword instead oftypename
. They’re interchangeable. - Templates can take in value arguments as well as type arguments:
Templates vs. Generics
Templates are massively different from generics in other OOP languages like Java.
In Java, generics are mainly syntactic sugar that help programmers avoid boilerplate code. In C++, templates are a code generation system that enables generic programming and more. Some main differences:
Think of C++‘s templates as very sophisticated preprocessor commands. It’s used to generate type-safe code. In Java’s generic system, type erasure is used, resulting in a single implementation of the generic class/function rather than many specialised instances of a template like in C++.
Function Templates
Function templates are a generalised algorithm that the compiler can generate copies of for usage with specific types.
T
is a type argument. Its scope is limited to the function signature and body.- Strictly speaking, a function template is not really a function. It’s a generalisation of an algorithm that is used as a tool by the compiler for generating similar but independent functions.
- The act of generating a function from a function template by the compiler is called template instantiation
A generic swap function:
- When you invoke
swap<int>(i, j)
, the compiler will instantiate a function with signaturevoid swap<int>(int& a, int& b)
for you by plugging in the types into the template function you defined.- Template instantiation is done on-demand when you compile.
- The compiler is smart enough to avoid instantiating duplicate functions.
Class Templates
Basically function templates, but with classes.
- Note: when within the scope of the class body, you can use
MyContainer
andMyContainer<T>
interchangeably. Essentially, you can consider<T>
optional inside the class body. However, when outside the class scope (i.e. before the::
), you have to fully qualify the name withMyContainer<T>::
- Once you specify the
::
inMyContainer<T>::
, you can imagine that you’re basically re-entering the class scope, and then everything you could access within the class become available again. (sourced from CppCon)
- Once you specify the
Aliases
You can use using
to create type aliases for template types.
Concepts [TODO]
Concepts are predicates that are used to apply constraints to the type arguments you pass to a template function or class. The main reason to use concepts is to get the compiler to be better at preventing misuage.
- This is from C++20. Prior to C++20, you’d rely on [[Knowledge/Engineering/Languages/C++#Type Traits [TODO]|type traits]] with
static_assert
s, or simply trusting programmers to follow documentation on how to use a template correctly. - Useful concepts are provided in the standard
<concepts>
header.
TODO: notes on concept-based overloading, defining concepts.
- You can overload template functions: depending on the type arguments, run a different generic algorithm.
Type Traits [TODO]
Deduction Guides [TODO]
This is for aiding type inference.
Functors
Functors, or function objects, are instances of a class that implements the function call operator method, operator()
, which means that they can invoked as if they were functions themselves. Functors are highly customisable, reusable, stateful functions.
Lambda Functions
You can think of lambda functions as syntactic sugar for inline, anonymous functors.
You can use the capture clause (the []
of the expression) to access variables from the outer scope.
[&foo, bar]
— capturesfoo
by reference andbar
by value.[&]
— capture all variables by reference.[&, foo]
— capture all variables by reference apart fromfoo
, which is captured by value.
[=]
— capture all variables by value.[=, &foo]
— capture all variables by value apart fromfoo
, which is captured by reference.
A classic use of lambda functions is passing it as the comparator function to std::sort
to determine whether one element comes before another.
Enums
In addition to structs and classes, you can also use enums to declare new data types. Enums are used to represent small sets of integer values in a readable way. There are two kinds of enums in C++, plain enums and enum classes (which are preferred over plain enums because of their type safety).
Plain Enum
Declared with just enum
. The enum’s values can be implicitly converted to integers
Enum Class
When you declare an enum with enum class
, it is strongly typed such that you won’t be able to assign an enum value to an integer variable or to another enum type. It reduces the number of ‘surprises’ which is why it’s preferred
Initialiser List
std::initializer_list
is a special class whose objects are used to pass around a sequence of data using curly braces {}
in a readable, intuitive way.
std::initializer_list
is an iterable.- For some reason, you cannot subscript an instance of
std::initializer_list
like you would a vector or array (SO discussion).
The compiler automatically converts { ... }
to an instantiation of std::initializer_list
in these situations:
{}
is used to construct a new object:Person person{"Tim", "Zhang"}
This will call the constructor of signaturePerson(std::initializer_list<string> l) {...}
if it exists. If such a constructor doesn’t exist, it will look forPerson(string firstName, string lastName) { ... }
. So basically, it prefers invoking constructors that take instd::initializer_list
but it will silently fall back to direct invocation if that fails.- Constructors taking only one argument of this type are a special kind of constructor, called initialiser-list constructor.
{}
is used on the RHS of an assignment:vector<int> vec = { 1, 2, 4 };
{}
is bound toauto
. E.g.
Interesting questions:
- It’s not possible to implement your own
std::initializer_list
. It’s coupled to the language standard and the logic of the compiler, which you can’t recreate through your own class. - Why isn’t
std::initializer_list
built-in?
Iterators
An iterator is an object that points to a specific item in a container. It has methods and operations for iterating over a container.
- To support range-based for loops, your class has to implement the
begin()
andend()
methods and make them return an iterator.
end()
isn’t the last element, it points one position beyond the last element.- There is also:
const_iterator
for read-only iteration.reverse_iterator
for reverse iteration. Use it withrbegin
andrend
Iterator Categories
There are different types of iterators in C++, in order to least functionality to richest functionality:
- Input iterator — you can only access the container in a single forward pass.
- Output iterator — you can only assign values to the container in a single forward pass.
- Forward iterator — combines input and output iterators.
- Bidirectional iterator — forward iterator that can also go back.
- Random access iterator — you can move the iterator anywhere, not just forward and back.
They form a hierarchy where forward iterators contain all the functionality of input and output iterators, bidirectional contains all of forward, and random-access contains all of bidirectional: (sourced from GeeksForGeeks) (sourced from GeeksForGeeks)
The STL containers support different iterator categories: (sourced from GeeksForGeeks)
Random C++ Features
Other important C++ details.
Structured Bindings
You can unpack values in C++17, similar to how you destructure objects in JavaScript. It’s just syntactic sugar.
- JavaScript calls this destructuring, Python calls this unpacking, C# calls this deconstructing.
- Unfortunately, you have to specify as many identifiers as there are things to unpack.
- Ranged for-loop:
- Tuple destructuring:
Using
There are a few different ways the using
keyword is used:
- Type aliasing (alternative to C-style
typedef
). It’s generally more preferred to useusing
over C-styletypedef
. It also supports a little more extra functionality that is not available withtypedef
, specifically for templates. Sourcetypedef
is locally scoped.using
makes the type alias available in the whole translation unit.
- Make an identifier from a namespace available in the current namespace.
- Make all identifiers from a namespace available in the current namespace.
- Avoid this as much as possible in large projects. It pollutes your namespace with lots of new identifiers.
using namespace
should never be used in header files because it forces the consumer of the header file to also bring in all those identifiers into their namespaces
- Lifting a parent class’ members into the current scope.
- Can be used to inherit constructors:
Copy Elision
By default, when you pass an object to a function, that object is copied over (pass-by-value). When it doesn’t affect program behaviour, the compiler can move the object rather than making a full copy of it. This compiler optimisation can also happen when returning an object, throwing an exception, etc.
- Note: in English/linguistics, to elide means to merge and therefore omit something in language. E.g. “dunno” == “don’t know”.
Copy elision is not enforced in the C++ standard, so don’t write code assuming this optimisation will happen.
Return Type Deduction
In C++14, you can infer the return type of function whose return type is left as auto
.
In A Tour of C++, Bjarne says to not overuse return type deductions.
Initialiser List
You can make a constructor or method take in an std::initializer_list
to let the caller directly pass in a curly brace list like {1, 2, 3, 4}
as an argument.
When the compiler sees something like {1, 2, 3, 4}
, it will convert it to an instance of std::initializer_list
(if used in a context like above).
Casting [TODO]
static_cast
— casts from one type to another. It does not check what you’re doing makes sense, so avoid using it often.dynamic_cast
— casts pointer/reference types. Useful for runtime type-checking of objects. E.g. you can convert an instance of a parent class into an instance of a child class.- Sometimes you have to use this, but do so sparingly.
“If we can avoid using type information, we can write simpler and more efficient code, but occasionally type information is lost and must be recovered. This typically happens when we pass an object to some system that accepts an interface specified by a base class. When that system later passes the object back to us, we might have to recover the original type.” — Bjarne Stroustrup, A Tour of C++.
- Sometimes you have to use this, but do so sparingly.
reinterpret_cast
— treating an object as a raw sequence of bytes.const_cast
— casts away ‘constness’.(type) value
— C-style typecasting. This is the least preferred way since it’s unconstrained.
Inline Functions
inline
functions: when you want a function to be compiled such that the code is put directly where it’s called instead of going through the overhead of entering a new function context, make that function inline
.
I.e. if you want the machine code to not have function calls to this function, put inline
before the function signature. This tells the compiler to place a copy of the function’s code at each point where the function is called at compile time.
- Inlining functions offers a marginal performance improvement (usually) because you avoid allocating a new stack frame that’s usually associated with making a function call.
- This performance improvement is done at the cost of a marginally bigger executable size since the code is now duplicated across potentially many places. Smaller functions are usually better candidates for inlining.
- Why not make everything inline?
if-statement With Initialiser
In C++17, you can declare variables inside if
statements and follow it up with a condition: if (init; condition) { ... }
.
Noexcept Conditions
noexcept(false)
: it’s possible to usenoexcept(false)
in function signatures to say that ‘this function throws no exceptions (but it actually might, lol)‘. Just avoid using it.noexcept(true)
andnoexcept
are completely equivalent.throw()
: in older C++, you can putthrow()
at the end of a function signature to say that the function never throws exceptions, for example:void something_bad() throw()
. It’s been deprecated bynoexcept
in C++11, which is preferred overthrow()
, so you’d do:void something_bad() noexcept
instead.
Aggregates
Aggregates are either arrays or structs/classes that you didn’t define constructors, private/protected instance variables or virtual methods for. When those conditions are met, that class is an aggregate type and can be initialised with {}
.
- The order that you declare the fields matter.
Even though they both use
{}
, aggregate initialisation is different from list initialisation!
PODs
PODs (Plain Old Data) are a kind of aggregate type that do not overload operator=
, have no destructor, and there are no non-static members that are: non-POD classes, arrays of non-POD classes, or references. Learn more.
Put simply, PODs are just simple data, or simple data containers, hence the name ‘plain old data’.
- They’re very similar to structs in C.
- Because they’re very simple, using PODs can offer performance advantages because the compiler no longer needs to set up the same abstractions necessary for normal classes to work, letting them generate more efficient code.
- Many built-in types are PODs, such as
int
,double
,enums
, etc.
Protected and Private Inheritance
Protected and private inheritance are specific to C++. Other languages usually won’t have this.
Just default to using public inheritance (
: public
) if you want to represent an is-a relationship. Use protected and private inheritance sparingly.
- Protected inheritance makes all public members inherited from the parent protected.
- Private inheritance makes all public and protected members inherited from the parent private. Private inheritance basically hides the inheritance from the rest of the world.
Class Prototypes
Class prototypes are just like function prototypes. You can declare all your classes upfront and then use them wherever you want throughout the code.
User-Defined Literals
It’s possible to define custom literals that improve readability. E.g. <chrono>
makes it possible to write: 24h
, 42min
, etc. directly.
Example:
thread_local
There is a thread_local
keyword in C++. When a variable is declared with thread_local
, it is brought into existence when the thread starts and deallocated when the thread ends. In that sense, the thread sees that variable as a static variable since it exists throughout its lifetime.
Unnamed Scopes
Usually, we use {}
to define scopes for functions, classes, if-blocks, for-loops, etc., but you can also just use them directly in code to create a restricted scope.
- Doing this within a function is useful when you want a destructor to be called as soon as possible. E.g. often when dealing with mutexes, you’d want to acquire and release a lock as soon as possible.
Union
A union
is data structure like a class
or struct
, except all its members share the same memory address, meaning it can only hold 1 value for one member at a time. The implication is that a union can only hold one value at a time, and its total allocated memory is equal to .
- It’s mainly used when you really need to conserve memory.
- They’re mostly useless in C++ but more useful in C.
Compile-Time If [TODO]
if constexpr() { ... }
. C++17.
Volatile [TODO]
Extern
From what I understand, extern int foo
is basically saying “trust me compiler, there’s an int called foo that is defined somewhere.”
Compiling the above with g++ -o main main.cc foo.cc
and it just works.
Fold Expressions [TODO]
Appendix:
All the notes under this section are meant to be topics or details you don’t need to care much about to program effectively with C++.
C++ Compilation [TODO]
Compilation of C++ programs follow 3 steps:
- Preprocessing
Preprocessor directives like
#include
,#define
,#if
, etc. transforms the code before any compilation happens. At the end of this step, a pure C++ file is produced. - Compilation The compiler (eg. g++, the GNU C++ compiler) takes in pure C++ source code and produces an object file. This step doesn’t produce anyting that the user can actually run — it just produces the machine language instructions.
- Linking Takes object files and produces a library or executable file that your OS can use.
Commenting
The advice here is sourced from Google’s C++ style guide.
File Comments
File comments are preferred but not always necessary. Function and class documentation, on the other hand, must be present with exceptions only for trivial cases.
- Start with licence boilerplate, then broadly describe what abstractions are introduced by the file.
- Don’t duplicate comments across a class’
.h
and.cc
file. Example
Variable Comments
Generally not required if the name is sufficiently descriptive. Often for class variables, more context is needed to explain the purpose of the variable.
TODO Comments
Function Comments
Always write a comment to explain what the function/method accomplishes unless it is trivial. This includes private functions.
- Start with a verb. Eg. “Opens a file…” or “Returns an iterator for…”
- Which arguments can be
nullptr
and what that would mean. - Performance implications of how the function is used.
Class Comments
Always write a comment to explain what the class’ purpose is and when to correctly use it. Always do this in the .h
file, leaving comments about implementation detail to the implementing .cc
file.
- Good place to provide a code snippet illustrating a simple use case.
- About documenting the
.h
header file vs. documenting the.cc
source file- Document how to use the function in the header file, or more accurately close to the declaration
- Document how the function works (if it’s not obvious from the code) in the source file, or more accurately, close to the definition Source.
Flashcards
Some simple Q-and-A notes to be used as flashcards.
- What is separate compilation?
- A division of a larger project into smaller units that interact with each other through header files. One unit only knows about another unit through their header files. The big benefit of structuring projects this way is to allow for compilation to be done independently on these units, meaning that if one unit changes while others have not, then only that one unit is to be compiled.
- What are the differences between copy, list and direct initialisation?
- They’re the 3 ways initialisation of a new variable is done in C++. Copy initialisation is done with
=
, list initialisation is done with{}
, and direct initialisation is done with()
. Copy initialisation invokes the . Direct initialisation is directly invoking the constructor, hence the use of()
.
- They’re the 3 ways initialisation of a new variable is done in C++. Copy initialisation is done with
- What is the difference between
const
andconstexpr
? - What happens when an identifier is declared outside of a namespace?
- That identifier becomes globally scoped, i.e. part of the global namespace. It can be referenced directly or with
::
to explicitly say it’s from the global namespace.
- That identifier becomes globally scoped, i.e. part of the global namespace. It can be referenced directly or with
- Explain RAII and what problem it aims to solve.
- Resource allocation is initialisation means that any resources (things like file handles, database handles, etc.) required by a class should be acquired in the constructor and then released in the destructor. Think of it as “scope-bound resource management”. The point here is that when a class throws an exception or goes out of scope, the destructor is called, guaranteeing no resources to be held after the object’s lifetime.
- What are designated initialisers in C++? How do you use them?
- How do you define a custom exception in C++?
- Write a new class that inherits from
std::exception
and implement theconst char* what() const throw()
method and implement a constructor that takes in an error message.
- Write a new class that inherits from
- Explain copy elision.
- What problem does a move constructor solve — when would you use one?
- What is a const method and why would you use it?
- A const method is one where
const
is placed at the end of the function signature. It basically says the method will not modify whateverthis
is. As a caller of a const method, you can trust it won’t mutate the object it was called on (although you still can’t be 100% certain because themutable
keyword exists, which lets the const method mutate specific fields anyway, and there are other ways even without themutable
keyword, I think).
- A const method is one where
- When would you write a function that has a
const
return type?- Pretty much never.
- What are inline functions? Why would you use them?
- What are aggregate types? What are POD types?
- Explain the differences between classes and structs.
- They’re basically the same, except the members in classes are private by default whereas the members in structs are public by default. Note that this difference is only for C++. In C# which also has classes and structs, the structs are also value types, meaning that they’re passed-by-value instead of by reference.
- Explain lvalues and rvalues.
- Explain what the access modifiers do in inheritance.
- How do you make a class abstract in C++?
- Give it at least one pure virtual function. E.g.
virtual void foo() = 0;
.
- Give it at least one pure virtual function. E.g.
- What are the differences between virtual functions and pure virtual functions? How does this differ between C++ and other languages like C# or Java?
- Virtual functions are those that can be overridden. Pure virtual functions are those that must be overridden. In other languages like C#, all methods are virtual by default. In those languages, pure virtual functions are known as abstract functions.
- How do you use
override
in C++? What’s the point? - Why is the output of the following program “Something”?
- If this code were written in Java, the output would be “Overridden”. In C++, we have object slicing. If you wanted to get this C++ to output “Overridden”, you’d have to use pointer types or references.
- Explain 3 ways the
delete
keyword is used.- Deleting memory-allocated objects:
delete obj;
, deleting arrays:delete[] my_arr;
, declaring deleted functionsvoid foo() = delete;
. Deleted functions are mainly used to suppress operations and constructors, mainly. E.g. you’d mark an overloaded operator method as deleted to prevent the misuse of it.
- Deleting memory-allocated objects:
- Explain const objects.
- Object variables declared with the
const
qualifier
- Object variables declared with the
- What are the uses for
final
?- It can be used to declare final methods and final classes. Final methods can’t be overridden or changed by child classes. They’re declared like this:
void foo() final;
. Final classes cannot be inherited. They’re declared like this:class Foo final;
.
- It can be used to declare final methods and final classes. Final methods can’t be overridden or changed by child classes. They’re declared like this:
- Explain
friend
in C++ — what do they do and how do you use it?- Inside class
Foo
, usefriend class Bar
to grantBar
access to every member ofFoo
. “Friends can touch your privates,” and “you can’t grant yourself access to other people’s privates, but you can grants others access to yours”. It’s use is discouraged, but sometimes it’s helpful for unit testing classes to access private methods.
- Inside class
- Explain lvalue and rvalue references.
- Write a valid function signature for: a copy constructor, copy assignment operator overload, move constructor and move assignment operator overload.
Foo(const Foo& other)
,Foo& operator=(const Foo& other)
,Foo(Foo&& other)
,Foo& operator=(Foo&& other)
.
- What is std::move?
- It’s a function you use to help you invoke the move constructor or the move assignment operator. It converts an lvalue to an rvalue reference. It doesn’t do any actual moving itself.
- What’s
thread_local
? - How do you define a function template?
- How do you define a class template? How do you define the methods of a class template outside of the class definition?
- What are value arguments in template definitions?
- You can make function or class templates take in a value in addition to type arguments. E.g.
template <typename T, int N>
.
- You can make function or class templates take in a value in addition to type arguments. E.g.
- What are functors?
- Functors are also called function objects.
- How are functors and lambda functions related?
- Lambda functions are basically anonymous functors.
- In lambda expression
[&, foo] () { ... }
, what does[&, foo]
mean?- It’s a capture group, which is a list of identifiers from the containing scope that should be accessible within the function body. The
[&, foo]
means that all identifiers should be accessible by reference, except forfoo
which should be copied.
- It’s a capture group, which is a list of identifiers from the containing scope that should be accessible within the function body. The
- What’s the difference between plain enums and enum classes? Which one should you generally prefer?
- What is the
extern
keyword in C++? - What is the type of
foo
here?auto foo = { 10, 20, 30 };
- What’s different about switch-case statements in C++ compared to other languages?
- You have to wrap each case in curly braces. E.g.
case foo: { ... }
.
- You have to wrap each case in curly braces. E.g.
- Should you always use range-based for loops?
- In general yes, but with exceptions. Eg. you should not use it when you are erasing values, inserting something into the middle of something, etc., basically anytime you need to manipulate the position of the iterator, you’d have to fall back to the ugly for loop.
- What are the 5 main iterator categories? How do they relate to each other?
- They are: input, output, forward, bidirectional, random access.
- Why should I use
make_unique
to createunique_ptr
s instead of directly constructing them usingnew
like in:unique_ptr<Foo>(new Foo(...))
?- It’s exception safe, meaning if
foo(unique_ptr<X>(new X), unique_ptr<Y>(new Y))
fails, there are no resource leaks. It avoids usage ofnew
anddelete
, which is always recommended for modern C++ code. It’s more readable since you specifyFoo
only once compared to twice:make_unique<Foo>
.
- It’s exception safe, meaning if
- Explain the difference between
unique_ptr
andshared_ptr
.unique_ptr
represents sole ownership whileshared_ptr
represents shared ownership. Aunique_ptr
deletes the object it’s hosting once it goes out of scope. Ashared_ptr
does the same only if it is the last remaining owner of the object it’s hosting. The copy constructor and assignment operator are disabled inunique_ptr
and only allows for move semantics. Inshared_ptr
, copy and move are enabled.
Questions
Some questions I have that are answered:
- Why use functors over methods? From what I know, the main purpose of functors is to act as stateful functions. Methods can clearly accomplish the same purpose.
- Why use regular array literal with
[]
vs usingstd::array
?