cpp-notes

Table of Contents

1. Compiler

1.1. Optimizations

You can use the -ggdb flag to compile in debug mode and -O2 -DNDEBUG to compile with the best optimizations.

1.2. Extensions

Disable compiler-specific extensions and strictly follow the C++ standard by adding the -pedantic-errors flag.

1.3. Enable Warnings

You should enable warnings for the best learning experience. Enable them using- -Wall -Weffc++ -Wextra -Wconversion -Wsign-conversion To treat warnings as errors, add -Werror flag.

1.4. Version Code Names

Version Code Name Before Release
C++11 c++1x
C++14 c++1y
C++17 c++1z
C++20 c++2a
C++23 c++2b

Compiler flags are -std=c++11, -std=c++14, -std=c++17, -std=c++20, -std=c++2b

2. Basics

2.1. Statements

A statement is an instruction in a computer program that tells the computer to perform an action.

  1. Declaration Statements
  2. Jump Statements
  3. Expression Statements
  4. Compound Statements
  5. Selection Statements (Conditionals)
  6. Iteration Statements
  7. Try Blocks

2.2. Variable Assignment

  1. Assignment When variables are assigned using the = operator, value on the right hand side is copied to the variable on the left hand side, known as copy assignment.
  2. Initialization
    1. Default Initialization No initial value is provided.
    2. Copy Initialization Initializer is provided after equal sign. Use of this is discouraged because it is considered inefficient. int a = 5;
    3. Direct Initialization Initializer is provided inside parenthesis. Use of this is also discouraged, because it is confusing to read and is superseded by list initialization. int a(5);
    4. List Initialization Modern way. It does not allow narrow conversions because narrow conversions can cause data loss. Introduced in C++17.

      int a {5}; // direct list initialization
      int a = {5}; // copy list initialization
      int a{}; // value list initialization
      int a{4.5} // ERROR while a(4.5) or a = 4.5 won't give any error
      

2.2.1. The Maybe Unused Attribute

If we don't want the compiler to complain about unused variables, we can define variables with [[maybe_unused]]. This was introduced in C++17.

[[maybe_unused]] float pi {3.14};

2.3. Implementation Defined Behavior

Implementation-defined behavior means the behavior of some syntax is left up to the implementation (the compiler) to define. Such behaviors must be consistent and documented, but different compilers may produce different results. One example of this is using the sizeof operator. It will product different results on different platforms.

2.4. Identifier Naming Conventions

  1. Variable names should begin with lowercase characters.
  2. Function names should begin with lowercase characters.
  3. Identifier names starting with capital letters represent user defined types.
  4. camelCase or snake_case both are fine but stay consistent.
  5. You may mix them like snake_case for variable names and camelCase for function names.
  6. Do not start names with underscore _ (bad practice but not impossible).

2.5. Literals and Operators

A literal is a constant value inserted directly in the source code.

int a = 5;
std::cout << a << std::endl;  // a is a variable not a literal constant
std::cout << 10 << std::endl; // 10 is a literal constant

Operators perform operations on its inputs and return an output. The number of inputs an operator can take is called its arity. Some operators like throw and delete do not return any value. Operators which are mainly used for their side effects like x = 5 always return their left operand (x in this case). For example, x = y = 5 is the same as writing x = (y = 5).

2.6. Expressions

Expressions are like phrases in English, they are part of statements just like phrases are part of sentences. Expressions do not end with a semi-colon and they cannot be executed themselves. Some examples of expressions-

a = 5              // has a side-effect of assigning "a" to 5 and evaluates to "a"
5 + 6              // evaluates to 11
"Hello, World!"    // evaluates to itself
std::cout << "Hey" // evaluates to std::cout

Ending an expression with a semi-colon ; will cause the statement to execute properly and such expressions are called Expression Statements. Sub-expressions are expressions used as operands in other expressions. Compound expressions have two or more operators like x = 5 + 3 has two operators, = and +.

3. Functions and Files

3.1. Return values

The main function can return integers to specify if the program ran successfully or not. Two macros, EXIT_SUCCESS and EXIT_FAILURE are defined in the cstdlib.

#include <cstdlib>

int main() {
  return EXIT_SUCCESS;
}

A value defining function which does not return any value will produce undefined results.

3.2. Parameters and Arguments

  • Parameters are defined in the function header and arguments are passed when calling it.
  • If you have a parameter that is no longer used in the body but can't remove the corresponding argument from all function calls, you can just remove the identifier-

    void test(int /*name*/) {
      // indentifier removed from parameter list
      // to avoid breaking previous function calls
      // and prevent unused parameter warning
    }
    

    IMPORTANT! Note that in clang, arguments are parsed from left to right while in g++, arguments are parsed from right to left!

3.3. The One Definition Rule

  1. Within a file, each function, variable, type or template can only have one definition (except variables in different local scopes) (violation causes compile error)
  2. With a program (multiple files), each variable can have only one definition (violation causes linker error)
  3. Types, templates, inline functions, and inline variables are allowed to have duplicate definitions in different files, so long as each definition is identical (violation causes undefined behaviour).

3.4. Namespaces

  • A namespace provides a scope region called the namespace scope which means that any thing defined in that scope won't be mistaken for anything else with the same name defined in some other namespace.
  • Only declarations and definitions can appear in a namespace.
  • The :: operator is called the scope resolution operator. It's left operand is the namespace name (if blank, global is used) and the right operator is the symbol.
  • When an identifier uses the :: operator, it's called a qualified name.
  • Using using namespace <name> is considered bad practice since it can lead to many conflicts in the future.

3.5. Preprocessor

  • A preprocessor is a program which makes various changes to the code file before compiling it. For example, stripping out comments, making sure the each file ends with a newline character etc, evaluating the preprocessor directives, etc..
  • It does not make changes to the original files, instead it creates temporary files for this called "translation units".
  • The translation unit is what is actually compiled by the compiler.

3.5.1. Preprocessor Directives

  • These are statements that begin with a # and end with a newline.
  • They have their own syntax of doing things.
  1. Include Directive

    Includes header files - #include <iostream>

  2. Define Directive
    • Used to define function macros or object macros.
    • Function macros are not considered unsafe.
    • Object-like macros are no longer used in favour of better alternatives.

      // Replace all occurance of NOTHING with nothing
      #define NOTHING
      
      // Replace all occurances of PI with 3.14
      #define PI 3.14
      
  3. Conditional Directives

    This includes the ifdef, ifndef and endif directives.

3.5.2. Header Files

  • Header files must not contain definitions.
  • It is a good practice for code files to #include their paired header file (if one exists), this will help the compiler catch errors at compile time instead of link time.
  • Many useful C libraries are now renamed in C++, for example, stdlib.h is named cstdlib in C++.
  • The bad way to include header files is to use relative paths in the #include directive. For example, #include "../myfile.h".
  • The better way is to specify the include directory to the compiler using the -I flag- g++ main.cpp -I/src/includes -o main. There is no space after -I in the command.

3.5.3. Header Guards

Used to prevent duplication definitions when including multiple header files.

#ifndef FUNCTIONS_H
#define FUNCIONS_H
{...}
#endif
  • You can also use #pragma once in modern compilers instead of the above header guards, but it's not in the C++ standard and some compilers may not support it.

4. Debugging

4.1. Some Debugging Tactics

  1. Comment out code
  2. Use std::cerr instead of std::cout because std::cerr is unbuffered so output is instant.
  3. Printing values.

Using debug statements isn't recommended since they can clutter your code.

4.2. Using a debugger

4.2.1. Step Into

Execute the next command and pause. If the next command is a function call, move to the top of the function.

4.2.2. Step Over

Execute the next command and pause. If the next command is a function call, execute the entire function at once.

4.2.3. Step Out

Execute all remaining lines in the current function and pause.

4.2.4. Run to Cursor

Execute the program upto the line the cursor is in and pause.

4.2.5. Continue

Continue running the program normally.

4.2.6. Breakpoints

Execute the program upto the breakpoint and pause.

5. Data Types and Related Stuff

5.1. Standard Integers

Since the size of int is different on different compilers on different platforms, standard int was declared in the cstdint library.

#include <cstdint>

int main() {
  std::int8_t one; // 1 byte integer
  std::uint8_t two; // 1 byte unsigned integer
  std::int16_t three; // 2 byte integer
  std::uint16_t four; // 2 byte unsigned integer
  std::int32_t five; // 4 byte integer
  std::uint32_t six; // 4 byte unsigned integer
  std::int64_t seven; // 8 byte integer
  std::uint64_t eight; // 8 byte unsigned integer
  return 0;
}

The above may be slower on some hardware. For example, 32 bit integers may be slower than 64 bit integers on a 64bit CPU so the following types were created:

#include <cstdint>

int main() {
  std::int_fast32_t a; // will give the fastest integer having atleast 32 bits
  std::int_least32_t b; // will give the smallest integer having atleast 32 bits
  return 0;
}

Note that in many compilers, int_fast8_t and int_least8_t behave like char instead of integer values so prefer using the 16bit versions. So, if you try to print a variable of type int_fast8_t with the value 65 (for example), it will print A instead!!

5.2. size_t

  • This type is used to represent size or length of objects.
  • The data type of the value returned by the sizeof operator is also std::size_t.
  • It is guaranteed to be unsigned and has the size of the largest possible integer the machine can hold.
  • Any object with a size larger than the largest value an object of type std::size_t can hold is considered ill-formed.

5.3. Floating Point Precision

  • By default, std::cout displays only upto 6 significant digits of floating point numbers. You can change the precision by using std::cout << std::setprecision(<any number>).
  • Prefer double over float for better precision.

5.4. Rounding Errors in Decimals

  • In binary, a simple decimal number like 0.1 is represented in an infinite sequence 0.00011001100… causing it to be less precise.

    #include <iostream>
    #include <iomanip>
    
    int main() {
      double a{0.1};
      std::cout << std::setprecision(20);
      std::cout << a << std::endl;
    
      double b{};
      b = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1; // should be equal to 1.0
      std::cout << b << std::endl;
    
      if (a == b) std::cout << "YES";
      else std::cout << "NO";
      return 0;
    }
    

5.5. NaN and Infinity

#include <iostream>

int main() {
  double zero {0.0};
  std::cout << 5.0/zero << std::endl; // outputs inf
  std::cout << zero/zero << std::endl; // outputs nan
  return 0;
}

5.6. Boolean Values

  • No need to import stdbool like in C to use true and false.
  • Sending true and false values to stdout prints 0 and 1 instead.
  • To print true and false instead, do this-

    #include <iostream>
    
    int main() {
      bool a {false};
      bool b {!a};
      std::cout << a << " " << b << std::endl;
      std::cout << std::boolalpha; // causes bools to print as "true" or "false"
      std::cout << a << " " << b << std::endl;
      return 0;
    }
    

5.7. Explicit Type Conversions

  • Unlike C, where we convert types using (<type>) variable to cast to different types, in C++, we use static_cast operator.

    #include <iostream>
    
    void print(double x) {
      std::cout << x << std::endl;
    }
    
    int main() {
      int a{5};
      print(static_cast<double>(a));
      return 0;
    }
    

    The static_cast operator will produce undefined behavior if the value being converted doesn’t fit in range of the new type.

5.8. Hexadecimal, Octal and Binary Literals

You may use these decimals by prefixing the following-

  • 0x for hexadecimal, eg- int a{0x1F}
  • 0 for octal, eg- int a{012}
  • 0b for binary, eg- int a{0b1100011} (c++14 onwards)

By default, std::cout outputs values in decimals. You can specify the output type using-

#include <iostream>
#include <bitset> // for std::bitset

int main() {
  int x{69420};
  // Decimal
  std::cout << x << std::endl;

  // Octal
  std::cout << std::oct;
  std::cout << x << std::endl;

  // Hexadecimal
  std::cout << std::hex;
  std::cout << x << std::endl;

  // Binary
  std::cout << std::bitset<16>{0b10110} << std::endl;
  return 0;
}

In C++20 and C++23, better options are available-

#include <iostream>
#include <format> // C++20
// #include <print> // C++23

int main() {
  std::cout << std::format("{:b}\n", 0b1100101);
  std::cout << std::format("{:#b}\n", 0b1100101);
  // std::print("{:#b}\n", 0b1100101); C++23
  return 0;
}
(set-frame-parameter nil 'alpha-background 10)
(add-to-list 'default-frame-alist '(alpha-background . 10))

6. Constants and Strings

6.1. Constant Expressions

An expression that contains only compile time constants.

const int a = { 5 + 7 }; // Will be evaluated at compile-time
int a = { 5 + 7 }; // Might be evaluated at runtime, depends on the compiler

Using const can let the compiler know that the variable will absolutely not change whatsoever, so it can optimize it!

Only const integral variables with a constant expression initializer are compile-time constants. So, doubles or floats do not count. Types like char, bool, int, long, unsigned long etc can be considered.

  • It is sometimes very hard to distinguish whether an expression evaluates to a compile-time constant or not, so we can use constexpr keyword to tell the compiler that it's a compile time constant (using const doesn't guarantee it because it just means that the variable's value cannot be changed).
constexpr int a { 5 + 7 }; // Will be evaluated at compile-time instead of run-time

6.2. The Inline Keyword

  • Every function call has a cost. Whenever a function call is made, the CPU has to save the current state of the program, jump to the function, execute it, and then jump back to the original location and restore the state. This is called function call overhead.
  • Inline functions are functions whose definition is expanded in-place instead of being called. This reduces the function call overhead.
  • The inline keyword just suggests the compiler to make the function inline, it is not guaranteed to do so.
  • Inline functions perform better when they are small and are called frequently.
  • Inline functions are allowed to be defined in multiple translation units in modern C++!
  • Every inline definition must be identical and are mostly defined in header files.
inline is_even(int x) {
  return x % 2 == 0;
}

6.3. constexpr and consteval

  • As stated above, constexpr is used to tell the compiler that a variable is a compile-time constant.
  • constexpr can be used with functions to tell the compiler that the function is a compile-time function.

    constexpr int greater(int x, int y) {
      return (x > y ? x : y);
    }
    
  • If the parameters passed to a constexpr function are not compile-time constants, the function will be evaluated at run-time.
  • If the return value is not being used in a context where a compile-time constant is required, the function may or may not be evaluated at run-time.
#include <iostream>
#include <type_traits> // for std::is_constant_evaluated

constexpr int greater(int x, int y) {
  return (x > y ? x : y);
}

int main() {
  constexpr int p {greater(5, 6)}; // always a compile time constant
  std::cout << p << std::endl;

  std::cout << greater(5, 6) << std::endl; // may be a compile time constant
  return 0;
}

6.4. Immediate Functions

  • We can force functions which will be used in a context where a compile-time constant is required to be evaluated at compile-time using the consteval keyword.
  • Only available in C++20.
#include <iostream>

consteval bool getbigger(int a, int b) {
  return (a > b ? a : b);
}
int main() {
  constexpr int a{getbigger(5, 6)};
  std::cout << a << std::endl; // ok

  std::cout << getbigger(5, 6) << std::endl; // ok

  int x {5}; // not constexpr
  std::cout << getbigger(x, 6) << std::endl; // error
  return 0;
}
  • These functions are compile-time functions so they aren't very flexible during run-time.
  • consteval and constexpr functions are implicitly inline because the compiler needs to see the full definition at all times where the function is called.

6.5. Strings

  • C like strings are avoided because they are hard to work with and can be unsafe.
#include <iostream>
#include <string>

int main() {
  std::string name {"Prayag"};
  name = "Prayag Jain"; // can be reassigned
  std::cout << name << std::endl;
  return 0;
}
  • You can use std::getline() to read strings in C++.
#include <iostream>
#include <string>

int main() {
  std::string str;
  std::cout << "Enter name: ";

  // std::cin >> std::ws is an expression which ultimately returns std::cin
  std::getline(std::cin >> std::ws, str);

  std::cout << "Hi there, " << str << "!" << std::endl;
  return 0;
}
  • std::ws is an input manipulator which allows getline to ignore any leading whitespace characters already present in the input buffer.
  • std::ws is not preserved across calls so it must be used with every getline call.
  • Use str.length() or std::ssize(str) (defined in <string>) to get the length of a string named str.
  • Initializing using std::string is expensive, so do not pass them by value in functions.
  • Normal string literals with double quotes around them are C-Style string literals, E.g. "hello".
  • Using the suffix std::string_literals::s will form an std::string literal, E.g. "hello"std::string_literals::s.

6.6. std::string_view

  • Whenever an std:string is initialized, the string literal provided is copied into the string object (expensive operation).
  • Even in functions, when an std::string is passed by value, it is copied into the function parameter (expensive operation).
  • std::string_view is a lightweight alternative to std::string.
  • Always prefer std::string_view over std::string unless you need to modify the string.
  • It lives under the <string_view> header.
  • Assigning a new string to a std::string_view will not change the original string, it will just point to the new string.
  • You can use the std::string_view_literals::sv suffix to form a std::string_view literal.
  • Unlike std::string, std::string_view can be used in constexpr contexts.
  • The remove_prefix(count) and remove_suffix(count) member functions can be used to remove a prefix or suffix from a string view (does NOT modify the referenced string, just the view).
#include <string_view>
#include <iostream>

int main() {
  std::string_view str {"Hello, World!"};
  std::cout << str << std::endl;;

  str.remove_prefix(2); // remove the first two characters
  str.remove_suffix(2); // remove the last two characters
  std::cout << str << std::endl;
  return 0;
}

7. Operators

7.1. Binary Division Operator

  • It's the / operator we use to divide its two operands.
  • If one of the operands is a float, the operator performs floating point division.
  • If both operands are integers, the operator performs integer division and removes the fractional part.
  • If we want to divide two integers and get a float, we can use the static_cast<type> operator to cast one of the operands to a float/double.

7.2. Comma Operator

  • x,y means first evaluate x, then evaluate y and finally return the value of y

7.3. Comparing floating points

  • Floating points are dangerous to compare using the traditional comparison operators like ==, < and > because of rounding errors.
  • Use the nearly equal algorithm which makes use of relative epsilons to check if two floating points are nearly equal.

8. Bit Manipulation

  • We can use individual bits in a byte to store boolean values or values that only depend on one bit.
  • When individual bits of an object are used as boolean values, we call them bit flags.
#include <bitset>
#include <iostream>

int main() {
  std::bitset<8> mybitset;
  return 0;
}
  • We reading bits, we number them from right to left (starting from 0).

8.1. Key Member Functions

8.1.1. test(position)

Used to test whether a bit is 0 or 1

8.1.2. set(position)

Used to turn a bit "on" (to 1).

8.1.3. reset(position)

Used to turn a bit "off" (to 0).

8.1.4. flip(position)

Used to flip a bit (to 1 if it was 0 and to 0 if it was 1).

#include <iostream>
#include <bitset>

int main() {
  std::bitset<5> bits{0b10101};
  std::cout << bits << '\n';

  bits.set(1);
  std::cout << bits << '\n';

  bits.flip(3);
  std::cout << bits << '\n';

  bits.reset(0);
  std::cout << bits << '\n';
  return 0;
}

8.1.5. size()

Returns the number of bits in the bitset.

8.1.6. count()

Returns the number of bits set to 1.

8.1.7. any()

Returns true if any bit is set to 1.

8.1.8. none()

Returns true if none of the bits are set to 1.

8.1.9. all()

Returns true if all of the bits are set to 1.

8.2. Bitwise Operators

  1. >> (right shift) (eg- 0011 >> 1 = 0001, 0011 >> 2 = 0000)
  2. << (left shift) (eg- 0011 << 1 = 0110, 0011 << 2 = 1100)
  3. & (bitwise and) (eg- 0101 & 0011 = 0001)
  4. | (bitwise or) (eg- 0101 | 0011 = 0111) (only one bit should be 1 to be evaluated as true)
  5. ^ (bitwise xor) (eg- 0101 ^ 0011 = 0110)
  6. ~ (bitwise not) (eg- ~0101 = 1010)

8.3. Bit Masks

  • Used to block bitwise operators from modifying bits we don't want in a bitset, much like we use masking tape to block paint from touching certain parts of the product we're painting.
  • We can define bit masks as normal integers using either binary literals (c++14 or above) or by converting binary numbers to other literals such as decimals or hexadecimals.

8.3.1. Testing Bits using Masks

#include <iostream>
#include <cstdint>

int main() {
  // creating masks
  [[maybe_unused]] constexpr std::uint8_t mask1 {0b00000001};
  [[maybe_unused]] constexpr std::uint8_t mask2 {0b00000010};
  [[maybe_unused]] constexpr std::uint8_t mask3 {0b00000100};
  [[maybe_unused]] constexpr std::uint8_t mask4 {0b00001000};
  [[maybe_unused]] constexpr std::uint8_t mask5 {0b00010000};
  [[maybe_unused]] constexpr std::uint8_t mask6 {0b00100000};
  [[maybe_unused]] constexpr std::uint8_t mask7 {0b01000000};
  [[maybe_unused]] constexpr std::uint8_t mask8 {0b10000000};

  // testing if a bit at position 4 (starting from 0 from the right) is on or off using masks
  std::uint8_t flags {0b01111011}; // arbitrary value
  std::cout << "4th bit is " << (static_cast<bool>(flags & mask5) ? "on" : "off") << std::endl;
  return 0;
}

8.3.2. Setting and Resetting Bits using Masks

#include <iostream>
#include <cstdint>

int main() {
  // creating masks
  [[maybe_unused]] constexpr std::uint8_t mask1 {0b00000001};
  [[maybe_unused]] constexpr std::uint8_t mask2 {0b00000010};
  [[maybe_unused]] constexpr std::uint8_t mask3 {0b00000100};
  [[maybe_unused]] constexpr std::uint8_t mask4 {0b00001000};
  [[maybe_unused]] constexpr std::uint8_t mask5 {0b00010000};
  [[maybe_unused]] constexpr std::uint8_t mask6 {0b00100000};
  [[maybe_unused]] constexpr std::uint8_t mask7 {0b01000000};
  [[maybe_unused]] constexpr std::uint8_t mask8 {0b10000000};

  std::uint8_t flags {0b01101101};

  // turning flag 4 on
  flags |= mask5; // using bitwise OR

  // turning flag 0 off
  flags &= ~mask1; // using bitwise AND and bitwise NOT

  // flipping bit 7
  flags ^= mask8; // using bitwise XOR

  // flipping bits 3 and 2 simultaneously
  flags ^= (mask4 | mask3);

  std::cout << "The 4th bit is " << (static_cast<bool>(flags & mask5) ? "on" : "off") << std::endl;
  std::cout << "The 0th bit is " << (static_cast<bool>(flags & mask1) ? "on" : "off") << std::endl;
  std::cout << "The 7th bit is " << (static_cast<bool>(flags & mask8) ? "on" : "off") << std::endl;
  std::cout << "The 3rd bit is " << (static_cast<bool>(flags & mask4) ? "on" : "off") << std::endl;
  std::cout << "The 2rd bit is " << (static_cast<bool>(flags & mask3) ? "on" : "off") << std::endl;
  return 0;
}
  • You may use std::bitset instead of std::uint8_t too!

8.4. Example Hex Colour to RGBA Colour

printf "FF03CA04" > /tmp/inp
#include <iostream>
#include <bitset>
#include <cstdint>

int main() {
  // defining masks
  constexpr std::uint32_t redBits {0xFF000000};
  constexpr std::uint32_t blueBits {0x00FF0000};
  constexpr std::uint32_t greenBits {0x0000FF00};
  constexpr std::uint32_t alphaBits {0x000000FF};

  std::uint32_t hexVal {};
  std::cin >> std::hex >> hexVal; // std::hex allows us to read in hex (input modifier)

  // getting individual colours as integers
  int red {static_cast<int>((hexVal & redBits) >> 24)};
  int blue {static_cast<int>((hexVal & blueBits) >> 16)};
  int green {static_cast<int>((hexVal & greenBits) >> 8)};
  int alpha {static_cast<int>(hexVal & alphaBits)};

  std::cout << "rgba(" << red << "," << blue << "," << green << "," << alpha << ")" << std::endl;
  return 0;
}

8.5. Two's Complement

  • Signed integers are typically stored in two's complement format.
  • In two's complement, the most significant bit (left most bit) is used to represent the sign of the number.
  • 0 means positive and 1 means negative.
  • To convert a decimal number to a negative binary number, we invert all the bits and add 1.
  • Example, to convert -5 to binary, we first convert 5 to binary (0101) and then invert all the bits (1010) and add 1 (1011).
  • We add 1 because we need to represent 0 as 0000 and not 1111.

9. Namespaces

  • A namespace provides a scope region called the namespace scope which means that any thing defined in that scope won't be mistaken for anything else with the same name defined in some other namespace.
#include <iostream>
#include <string>

void print(std::string x, int y) {
  std::cout << x << y << std::endl;
}

namespace mynamespace {
  int a {5};
  void print(); // defining an arbitrary function in this namespace
  void doSomething(int b) {
    ::print("hello ", b); // use print from the global namespace instead of this namespace
  }
}

int main() {
  mynamespace::doSomething(mynamespace::a);
  using namespace mynamespace;
  doSomething(a);
  return 0;
}

Multiple definitions of the same namespace is allowed in C++.

  • Nested namespaces can be accessed by doing something like namespaceOne::namespaceTwo::hello.

9.1. Namespace Aliases

#include <iostream>

namespace Foo {
  namespace Goo {
    int a {5};
  }
}

int main() {
  namespace fg = Foo::Goo;
  std::cout << fg::a << std::endl;
  return 0;
}

Avoid deeply nested namespaces!

10. Scope, Duration and Lifetime

  • Scope is the region of code where an identifier is visible (global, local, namespace, block).
  • Duration specifies the start and end of the lifetime of an object (auto, static).
  • Lifetime is the time for which an object exists in memory.
  • Global variables have static duration by default.
  • Non constant integral global variables are initialized to 0 by default.

10.1. Name Hiding (Shadowing)

  • Whenever a name is defined in a scope, it hides any other name with the same name in the outer scope.
  • You should generally avoid variable shadowing.
  • In g++, you can use the -Wshadow flag to get warnings about shadowing.

10.2. Internal Linkage

  • Linkage is the property of an identifier which specifies whether it can be used in other translation units.
  • There are three types of linkages-
    1. External Linkage
    2. Internal Linkage
    3. No Linkage
  • Local variables have no linkage.
  • Non constant global variables have external linkage by default while constant global variables have internal linkage by default.
  • To make global variables have internal linkage, use the static keyword.
  • Functions have external linkage by default but can also be made to have internal linkage using the static keyword.

It is recommended to give internal linkage to all global variables and functions that are not used in other translation units.

// Internal global variables definitions:
static int g_x;          // defines non-initialized internal global variable (zero initialized by default)
static int g_x{ 1 };     // defines initialized internal global variable

const int g_y { 2 };     // defines initialized internal global const variable
constexpr int g_y { 3 }; // defines initialized internal global constexpr variable

// Internal function definitions:
static int foo() {};     // defines internal function

10.3. External Linkage

  • To make a variable or function have external linkage, use the extern keyword.
  • Global variables and functions have external linkage by default so you don't need to use the extern keyword.

If you want to define an uninitialized non-const global variable, do not use the extern keyword, otherwise C++ will think you’re trying to make a forward declaration for the variable

  • constexpr can be made to have external linkage by using the extern keyword however they can not be forward declared.
// External global variable definitions:
int g_x;                       // defines non-initialized external global variable (zero initialized by default)
extern const int g_x{ 1 };     // defines initialized const external global variable
extern constexpr int g_x{ 2 }; // defines initialized constexpr external global variable

// Forward declarations
extern int g_y;                // forward declaration for non-constant global variable
extern const int g_y;          // forward declaration for const global variable
extern constexpr int g_y;      // not allowed: constexpr variables can't be forward declared

10.4. Non-constant Global Variables are Evil

  • They can be changed from anywhere in the program.

Dynamic initialization of global variables causes a lot of problems in C++. Avoid dynamic initialization whenever possible.

  • Prefer using namespaces to avoid name collisions.
  • In functions, prefer passing variables as parameters instead of using global variables.

10.5. Sharing Global Variables Across Files

  • You can create header files, define a namespace, define the constants as constexpr and then include the header file in all files where you want to use the constants.
  • This approach has some downsides-
    1. If the header file is used in 20 files, the definitions will be duplicated 20 times and each file will have to be recompiled if the header file is changed.
    2. If the constants are large, it will increase memory usage because of duplication!
  • Another approach is to define a namespace in a .cpp file, define the constants using extern const in the namespace, forward declare the constants in a .h header file and then include the header file in all files where you want to use the constants.
  • This approach also has some downsides-
    1. These constants will now be considered compile-time only in the .cpp file in which they are defined because the linker will only see the forward declaration from the header file.

10.6. Global Constants as Inline Variables

  • For C++17 and above, you can use inline variables to define global constants.
  • These variables are better than macros because they are type safe and can be debugged.
  • They are better than global constants because they are not duplicated across files.
  • Prefer using them over the above two methods of sharing global variables across files.

    constants.h

    #IFNDEF CONSTANTS_H
    #DEFINE CONSTANTS_H
    namespace constants {
      inline constexpr double pi {3.14159};
    }
    #ENDIF
    

10.7. Static on local variables

  • Using the static keyword on local variables changes its duration to be created at the start of the program and destroyed at the end.
  • It is common to use the s_ prefix on static variable identifiers.
  • You should avoid static local variables unless the variable never needs to be reset as this can cause confusion.

10.8. Summary

Type Example Scope Duration Linkage Notes
Local variable int x; Block Automatic None  
Static local variable static int s_x; Block Static None  
Dynamic local variable int* x { new int{} }; Block Dynamic None  
Function parameter void foo(int x) Block Automatic None  
External non-constant global variable int g_x; Global Static External Initialized or uninitialized
Internal non-constant global variable static int g_x; Global Static Internal Initialized or uninitialized
Internal constant global variable constexpr int g_x { 1 }; Global Static Internal Must be initialized
External constant global variable extern const int g_x { 1 }; Global Static External Must be initialized
Inline constant global variable (C++17) inline constexpr int g_x { 1 }; Global Static External Must be initialized

11. Using Declarations

11.1. Qualified and unqualified names

A qualified name is one which has been resolved using the namespace operator (::) or the member selection operators (., ->). For example, std::cout, std::pow(5, 3), student.name etc..

11.2. Using-declarations

#include <iostream>

int main() {
  using std::cout; // specifying the exact object that we'd like to use
  cout << "Hello World\n";
  return 0;
}

11.3. Using-directives

#include <iostream>

int main() {
  using namespace std; // importing the entire namespace into the current scope
  return 0;
}

Prefer explicit namespaces over using-statements. Avoid using-directives whenever possible. Using-declarations are okay to use inside blocks.

12. Control Flow

  • The specific sequence of statements that the CPU executes is called the program's execution path.

12.1. Null Statements

  • Statements that just contain the semicolon ;.
#include <iostream>

int main() {
  if (1)
    ;
  return 0;
}

12.2. constexpr if statements

  • Consider the following code-

    #include <iostream>
    
    int main() {
      constexpr double pi {3.14};
      if (pi == 3.14) {
        std::cout << ("Hello World") << std::endl;
      } else {
        std::cout << ("Goodbye World") << std::endl; // This will never be printed
      }
      return 0;
    }
    
  • The condition above is always true.
  • This is wasteful at runtime so C++17 introduced constexpr if statements-

    #include <iostream>
    
    int main() {
      constexpr double pi {3.14};
      if constexpr (pi == 3.14)
        std::cout << "pi is 3.14" << std::endl;
      else
        std::cout << "pi is not 3.14" << std::endl;
      return 0;
    }
    

12.3. The [[fallthrough]] attribute

  • When using switch statements, if we do not use the break or return keyword, the cases following the matched case will also be executed which is known as a fallthrough.
  • This is not desired mostly but when it is, we can tell the compiler to avoid giving a warning by using the [[fallthrough]] attribute.
#include <iostream>

int main() {
  switch (2) {
  case 1:
    std::cout << "1" << std::endl;
    break;
  case 2:
    std::cout << "2" << std::endl;
    [[fallthrough]];
  case 3:
    std::cout << "3" << std::endl;
    break;
  }
  return 0;
}

Note that the semicolon used along with the [[fallthrough]] attribute is a null statement.

12.4. Halting Programs

  • You can halt a program using std::exit() from cstdlib.
  • This function does not, however, clean up memory reserved for the local variables.
  • Always use the std::atexit(callback) function which runs the callback function provided to it whenever std::exit() has been called.

You should never use halt functions explicitly unless there's no safe way to exit the program.

13. Randomness

  • Algorithms that simulate generating random numbers are called "Pseudo Random Number Generators (PRNGs)".
  • An initial value called the seed is provided to every PRNG to generate random numbers.
  • To further generate random numbers, the initial seed is modified by some mathematical calculations and then the new value is used.
  • The state of an algorithm is the value that the stateful algorithm keeps.
  • An algorithm is considered to be stateful if it retains some information across calls.
  • When, for a PRNG, the size of the seed provided is less than the intended size of the state, the PRNG is said to be underseeded.
  • Underseeded PRNGs will produce low quality random numbers.

13.1. Built-in PRNGs in C++

  • In the <random> library, there are 6 PRNGs available for use (as of C++20).
Type name Family Period State size* Performance Quality Should I use this?
minstd_rand Linear congruential generator 231 4 bytes Bad Awful No
minstd_rand0 Linear congruential generator 231 4 bytes Bad Awful No
mt19937 Mersenne twister 219937 2500 bytes Decent Decent Probably (see next section)
mt19937_64 Mersenne twister 219937 2500 bytes Decent Decent Probably (see next section)
ranlux24 Subtract and carry 10171 96 bytes Awful Good No
ranlux48 Subtract and carry 10171 96 bytes Awful Good No
knuthb Shuffled linear congruential generator 231 1028 bytes Awful Bad No
defaultrandomengine Any of above (implementation defined) Varies Varies ? ? No2
rand() Linear congruential generator 231 4 bytes Bad Awful Nono
  • We should only use the Mersenne twister PRNG out of all the other built-in methods (if you don't have the choice to use third party libraries).
  • mt19937 generates 32 bit unsigned integers while mt19937_64 generates 64 bit unsigned integers.
#include <iostream>
#include <random>

int main() {
  std::mt19937 mt{}; // initializing
  std::cout << mt() << std::endl; // using mt() is the preferred way to call the mt.operator() method
  return 0;
}
  • To generate numbers within a range with unbiased results (i.e. each number has an equal chance of being generated), we can use the std::uniform_int_distribution class.
#include <iostream>
#include <random>

int main() {
  std::mt19937 mt{};

  std::uniform_int_distribution dice{1,6};

  // printing a bunch of random numbers
  for (int count {1}; count <= 40; count++) {
    std::cout << dice(mt) << ' ';
  }
  return 0;
}
  • We can use std::chrono to seed the PRNG with the current time (it stores the time in "ticks" which is usually in nanoseconds or microseconds!).
#include <iostream>
#include <random>
#include <chrono>

int main() {
  std::mt19937 mt{static_cast<std::mt19937::result_type>(std::chrono::steady_clock::now().time_since_epoch().count())};

  std::uniform_int_distribution dice{1,6};

  for (int i {1}; i <= 40; i++) {
    std::cout << dice(mt) << ' ';
  }
}
  • You can also seed using the system's random device (recommended over system time)-
#include <iostream>
#include <random>

int main() {
  std::mt19937 mt{std::random_device{}()};

  std::uniform_int_distribution dice{1,6};
  for (int i {0}; i < 40; i++)
    std::cout << dice(mt) << ' ';
  return 0;
}

std::random_device also generates random numbers but should not be used as a PRNG because it is implementation defined, may not be available on all platforms, and might produce low quality results on different compilers.

  • The internal state of Mersenne twister is 624 bytes in size.
  • The seeds we provided above were only 4 bytes in size causing the PRNG to be heavily underseeded.
  • std::seed_seq can be used to provide a seed sequence to the PRNG.
  • We can provide as many random values to std::seed_seq as we want and it generate unbiased seed values with the required size for the PRNG.
#include <iostream>
#include <random>

int main() {
  std::random_device rd{};
  std::seed_seq ss{rd(), rd(), rd(), rd(), rd(), rd(), rd(), rd()}; // 8 random integers
  std::mt19937 mt{ss}; // initializing using the seed sequence

  std::uniform_int_distribution dice{1,6};

  for(int i {0}; i < 40; i++)
    std::cout << dice(mt) << ' ';

  return 0;
}

14. Software Testing

14.1. Informal Testing

  • Informal testing is the process of testing a program by running it and observing its behaviour.
  • After writing a program, you just run it with a few different inputs and see if it works as expected.
  • We can write functions that test's the program's functions by comparing their outputs with expected results.
  • You can also use assert to check if a condition is true and if it isn't, the program will halt.

14.2. Code Coverage

  • Code coverage is a measure of how much of the code is executed when the program is executed while testing.
  • Similarly, the term statement coverage refers to the percentage of statements that are executed during testing.
  • Branch coverage refers to the percentage of branches that are executed during testing.
  • Loop coverage says that if you have a loop, you should test it with 0, 1, and 2 iterations. If it works for the second iteration, it will work for all other iterations.

14.3. Symantic Errors

  • Errors that occur when the program is running and are not caught by the compiler (logical type of errors).
  • Some semantic errors include:
    • Division by zero
    • Precision errors
    • Comparison logical errors
    • Not using blocks for if statements

14.4. Detecting and Handling Errors

  • Most of the time, errors are caused because the programmer made some faulty assumptions like the user will always enter a number when the program asks for a number, the student being look up will always be in the database, etc.

14.5. Handling String Input

14.5.1. Error Case 1: Extraction succeeds but input is meaningless

  • For example, required input values were "y" or "n" but the input entered was "q".
  • Solving these is easy. Just use a while loop until the user enters one of the required values.

14.5.2. Error Case 2: Input buffer already had some characters

  • In cases where the input buffer already had some items.
  • You can tell the compiler to clear the input buffer after taking input using std::cin.ignore(100, '\n').
  • The above method will clear 100 characters out of the buffer OR until the newline is encountered.
  • To ignore everything upto and including the \n, we use-

    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n')
    
  • We can use std::cin.peek() to peek at the next character in the input buffer like this-

    while (true) {
      char c{};
      std::cin >> c;
    
      if (!std::cin.eof() && std::cin.peek() != '\n') {
        // clear input buffer
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        continue;
      }
    }
    

14.5.3. Error Case 3: Extraction fails and program goes in an infinite loop

  • Whenever an invalid value is present in the input buffer (for example, a char when int was asked), the value stays in the buffer and the program again searches the buffer (going into an infinite loop).
  • We can prevent this by doing-

    if (std::cin.fail()) {
      // If EOF character was inserted (on pressing Ctrl+D)
      if (std::cin.eof()) {
          exit(0); // shut down the program now
      }
      std::cin.clear(); // used to unset the "failbit" after bad input, i.e., putting back to normal mode
      std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }
    
  • We can also just test for failure like this- if (!std::cin).

14.6. Assertions

  • Preconditions are the conditions that must be true before a function is called.
  • An invariant is a condition that must be true while some section of code is executing. This is often used with loops, where the loop body will only execute so long as the invariant is true.
  • Postconditions are the conditions that must be true after a function is called.

14.6.1. Assert

  • An assertion is a statement that which takes a condition which, if false, will print an error message and run std::abort().
  • You can use the assert macro to check if a condition is true.

    void printAge(int age) {
      assert(age >= 0 && "Age must be positive");
      std::cout << "I am " << age << " years old" << std::endl;
    }
    
    • Assert statements should NOT be used in production code as they come with a performance cost and the program must have already been checked for any errors.
    • The NDEBUG flag disables assert statements.

14.6.2. Static Assert

  • Instead of a macro, it is a keyword.
  • It is used to check if a condition is true at compile time.

15. Type Conversions, Type Aliases, Type Deduction

15.1. Implicit Type Conversions

  • Performed automatically by the compiler.
  • If the conversion can not be performed, the compiler will throw an error.

15.1.1. Numeric Promotions

  • A numeric promotion is an implicit type conversion where narrower data types (like char) are converted to wider data types (like int and long) that can be processed efficiently.
  1. Floating Point Promotions

    A value of type float is promoted to double.

  2. Integral Promotions

    A value of type char, signed char, unsigned char, short, or unsigned short is promoted to an int if an int can represent all the values of the original type.

  3. Safe Conversions

    Conversions in which the original value (the meaning) does not change. For example, converting an int to a long.

  4. Reinterpretative Conversions

    Conversions which are considered unsafe where the result may be outside the range of the source type. This includes signed/unsigned conversions.

  5. Lossy Conversions

    Conversions in which the some/whole part of the original value is lost. For example, converting a double to an int or double with many decimal places to a float (low precision causes loss of some decimal values).

15.2. Narrowing Conversions

  • From a floating point type to an integer type (unless it's constexpr or decimal places are zero).
  • From an integral type to a floating type.
  • From an integral type to a narrower integral type (unless it's constexpr or in range).
  • These are unsafe and should be avoided.
  • In some cases, you may want to explicitly do a narrowing conversion. You can do this using the static_cast operator.

15.3. Type Aliases

  • We use the using keyword to define type aliases.
using Distance = double;
Distance homeToSchool {3.55};
  • It's a convention to name aliases starting with a capital letter.
  • We can also use typedef from C in C++ (but it's only present for backwards compatibility and its syntax can get very confusing).
  • The std::int8_t type is just an alias to char which is exactly why we get a character value when using it.

15.4. Type Deduction

  • We defining a variable, for example double distance {5.76};, the literal 5.76 has the type double and we have explicitly mentioned double as the data type of distance, providing the same information twice.
  • The compiler can deduce the type automatically in such cases using- auto distance {5.76};
  • Type deduction doesn't work if the return value of a function is void, for example-

    #include <iostream>
    
    void test() {
      // test
    }
    
    int main() {
      auto a {test()}; // Not valid
      return 0;
    }
    
  • Using auto will drop const and constexpr from the type.
constexpr int num {5};
auto a {num}; // a is of type int, not constexpr int

constexpr auto b {num}; // b is of type constexpr int
  • Normal string literals default to the type const char*, so to use std::string or std::string_view, use literal suffixes- "hello"std::literals::s or "hello"std::literals::sv.
  • In such cases, it may be better to not use auto and just use the type explicitly.
  • You can also use auto with functions, however this is NOT recommended.

    auto add(int a, int b) {
      return a + b;
    }
    
  • The following is also valid

    auto add(int a, int b) -> int {
      return a + b;
    }
    
  • Type deductions can't be used for function parameters, like int add(auto a, autob); is not valid.

16. Function Overloading

  • Function overloading is the process of defining multiple functions with the same name but different parameters.
  • The compiler will choose the function to call based on the type of the parameters.
  • Function overloading is allowed so long as the compiler can differntiate between the functions (else, compiler error).
#include <iostream>
int add(int a, int b) {
  int result = a + b;
  return result;
}

double add(double a, double b) {
  double result = a + b;
  return result;
}

int main() {
  std::cout << add(5, 3) << std::endl;
  std::cout << add(5.5, 3.3) << std::endl;
  return 0;
}
  • Unexpectedly, there are no downsides to function overloading and it can/should be used liberally.

16.1. Function Overload Differentiation

  • The compiler differentiates between functions by looking at the number of parameters and their types.
  • The return type of a function is not considered when differentiating between functions.
  • Qualifiers like const are also not considered when differentiating between functions.

16.2. Resolving Overloaded Function Calls

  • Resolving overloaded function calls is the process of the compiler choosing which function to call when an overloaded function is called.
  • It goes through these steps-
    1. The compiler will look for an exact match for the function call.
    2. If step 1 fails, the compiler will perform numeric promotions and look for a match.
    3. If step 2 fails, the compiler will perform numeric conversions and look for a match.
    4. If step 3 fails, the compiler will look for a match using the user-defined conversions.
    5. If step 4 fails, the compiler will look for a matching function that uses ellipsis.
    6. If step 5 fails, the compiler will throw an error.

16.3. Deleting Functions

  • You can prohibit certain function calls by using the delete keyword.
#include <iostream>
int add(int a, int b) {
  return a + b;
}

void add(char a, char b) = delete; // if called, the program will not compile
void add(double a, double b) = delete; // if called, the program will not compile

int main() {
  add(5, 3);
  add('a', 'b'); // will not compile
  add(5.5, 3.3); // will not compile
  return 0;
}

16.4. Default Arguments

  • Default arguments are used to provide a default value for a function parameter.
  • The default arguments should be specified in forward declarations (function prototype) and not in the function definition.
  • Example: int add(int a, int b = 0);

BEST PRACTICE: If the function has a forward declaration (especially one in a header file), put the default argument there. Otherwise, put the default argument in the function definition. For example, in the forward declaration do this int add(int x=5, int y=7) and in the definition, do this int add(int x, int y).

17. Function Templates

  • The generic type is specified using a template parameter.
template <typename T>
T add(T a, T b) {
  return a + b;
}
  • The typename or the class keyword is used to specify a template parameter (it doesn't matter if you use class or typename, both mean the same).
  • A function template is used to generate functions. Function templates themselves are not functions.
  • We can call the add function doing something like this- add<int>(5, 6). The compiler will see that a function definition for add does not already exist, so it will generate one.
  • When the data types of arguments matches the typename of the template, we can simply let the compiler deduce the typename doing something like this- add<>(5, 6) or simply add(5,6).
#include <iostream>
template <typename T>
T getMax(T a, T b) {
  return (a > b) ? a : b;
}

char getMax(char a, char b) {
  return (a > b) ? a : b;
}

int main() {
  std::cout << getMax<int>(5, 10) << std::endl; // calls getMax<int>(int, int)
  std::cout << getMax<>(5, 10) << std::endl; // calls getMax<int>(int, int), not template functions are not considered
  std::cout << getMax(5, 10) << std::endl; // will call getMax(char, char)
  return 0;
}

17.1. Forward Declaring Function Templates

template<>
T add(T a, T b);

17.2. Including Function Templates

  • You can define function templates inside header files since they are exempted from the one definition rule.
  • This allows the compiler to see the full definition of the template and instantiate functions whenever needed.
  • Using forward declarations won't work for function templates.

17.3. Abbreviated Function Templates

  • For C++20 and above only.
  • Defines a simple shorthand to create function templates-

    // This is a shorthand to the template definition below
    auto add(auto a, auto b) {
      return a + b;
    }
    
    // The above is the shorthand to this
    template <typename T, typename U>
    audo add(T a, U b) {
            return a + b;
    }
    

17.4. Non-type Template Parameters

  • A template parameter used to represent a constexpr value.
  • The following types are accepted as template parameters-
    1. Integral types
    2. Enumeration type
    3. Floating Point type (since C++20)
    4. Literal class types (since C++20)
    5. Etc…
  • From C++17 onwards, you can use the auto keyword to automatically let the compiler deduce the non-type template parameter from the template argument.
#include <iostream>
template <int N>
void print() {
  std::cout << N << std::endl;
}

int main() {
  print<5>();
  return 0;
}

18. References

18.1. Lvalue and Rvalue Expressions

  • Rvalue expressions are expressions which evaluate to a value.
  • Lvalue expressions are expressions which evaluate to identifiable objects.

If you're not sure if an expression is an lvalue or not, remember that all lvalues can be referenced using the & operator while rvalues can not. For example, &x, where x is a variable (something like &5 won't work since 5 is an rvalue expression).

  • Lvalue expressions are implicitly converted to rvalue expressions when they are provided in places where rvalues were expected.
  • Lvalues are of two types, modifiable (non-const) and non-modifiable (const).
  • C-style string literalls are rvalues.

18.2. Lvalue References

  • An lvalue reference acts as an alias for an existing lvalue.
  • It is declared using the & operator

    int // int type
    int& // lvalue reference to an int
    double& // lvalue reference to a double
    
  • We can create lvalue reference variables like this
int age {10};
int& referenceToAge {age};
  • References must always be initialized.
  • Once initialized, a reference can not be changed to refer to another object.
  • References are not objects in C++. They aren't required to be stored in memory. The compiler might also replace all occurances of the reference with the referent to optimize.
  • The above point is the reason why you can not have references to references in C++.
int x {10};
int& ref {x}; // reference to x
int& ref2 {ref}; // this will work because it's NOT a reference to ref, it's a reference to x
  • Lvalue references can not bind to non-modifiable lvalues (non-const).
const int x {10};
int& ref {x}; // not allowed
  • Type deductions using the auto keyword will drop references.
int x {10};
int& y {x}; // reference to x

auto z {y}; // z is of type int, not int&
auto& z2 {y}; // z2 is of type int&
  • Type deductions using auto will drop references first and only then it will drop top-level consts.
#include <string>
const std::string& getConstRef(); // some function that returns a const reference
int main() {
    auto ref1{ getConstRef() };        // std::string (reference and top-level const dropped)
    const auto ref2{ getConstRef() };  // const std::string (reference dropped, const reapplied)
    auto& ref3{ getConstRef() };       // const std::string& (reference reapplied, low-level const not dropped)
    const auto& ref4{ getConstRef() }; // const std::string& (reference and const reapplied)
    return 0;
}

18.3. Dangling References

  • When the object that a reference refers to is destroyed, the reference becomes a dangling reference.

18.4. Lvalue references to const

  • To make lvalue references bind to non-modifiable lvalues, use the const keyword.
const int x {10};
const int& ref {x}; // allowed

int y {20};
const int& ref2 {y}; // allowed, but ref2 can not be used to modify y

Always use lvalue references to const when you don't need to modify the referent.

  • Lvalue references to const can also bind to rvalues.

    const int& ref {5}; // allowed
    
  • In the above example, a temporary object will be created to store the value 5 and the reference will bind to that temporary object.
  • This will increase the lifetime of the temporary object to the lifetime of the reference.

18.5. Pass Values by Reference To Functions

  • Reduces the overhead of copying large objects.
#include <iostream>

void hello(std::string& name) {
  std::cout << name << std::endl;
}

int main() {
  std::string text {"Hello World"};
  hello(text);
  return 0;
}

As a rule of thumb, pass fundamental types by value and class (or struct) types by references. Other common types to pass by value: enumerated types and std::stringview. Other common types to pass by (const) reference: std::string, std::array, and std::vector.

  • For objects that are cheap to copy, the cost of copying is similar to the cost of binding, so we favor pass by value so the code generated will be faster.
  • For objects that are expensive to copy, the cost of the copy dominates, so we favor pass by (const) reference to avoid making a copy.
  • std::string_view is better than std::string& because when passing different types of strings (c-style string literals, stringview, string), the std::string_view will be able to reference all those types easily, while passing types other than std::string to a std::string& will require the compiler to make a temporary std::string object (by implicit conversion or copy).

18.5.1.

18.6. Return by Reference

  • When returning values from a function, a copy is returned.
  • This may be expensive for class types.
#include <iostream>
// notice the ampersand (&) in the return type
const int& foo() {
  static constexpr int age {20}; // destroyed at the end of the program
  return age;
}
int main() {
  std::cout << foo() << std::endl;
  return 0;
}

Avoid returning non-const static values as references.

  • Initializing a reference returned by a function to a non reference variable will make a copy of the return value which defeats the entire purpose of returning a reference.
const int& foo() {
  static const int age {20};
  return age;
}

int main() {
  const int age {foo()}; // BAD: Defeats the purpose of returning reference from foo()
  const int& age2 {foo()}; // GOOD
  return 0;
}

18.7. In-Out Parameters

  • Parameters which are used to receive input from the function caller are called in parameters.
  • Parameters which are used to send output to the function caller are called out parameters (for example, references and pointers which are used to modify the original object from inside the function).

19. Pointers

  • Always initialize your pointers, for example, int* a{&x};.
  • Unlike references, pointers are objects and they require space in memory to exist.
  • References are safer than pointers (which are inherently dangerous).
  • The size of a pointer depends on the architecture the program's being compiled for. On a 32-bit machine, a pointer will be 32 bit in size and 64bits on a 64-bit machine.

19.1. Dangling Pointers

  • Pointers which store invalid addresses are called dangling pointers.
  • Dereferencing such pointers leads to undefined behaviour.

19.2. Null Pointers

  • Can be initialized using direct list initialization like this- int* ptr {};
  • They don't point to any address.
  • Can later be assigned an address.
  • We can use the nullptr literal to initialize a null pointer explicitly- int* ptr {nullptr};
  • nullptr has the type std::nullptr_t. So whenever nullptr is used to initialize a pointer variable, it is implicitly converted from std::nullptr_t to the required type.
int main() {
  int* ptr {nullptr}; // can initialize using nullptr
  int* anotherPtr {};

  anotherPtr = nullptr; // can also assign to nullptr literal later

  foo(nullptr); // can also pass nullptr literal to functions
  if (ptr == nullptr) std::cout << "No";

  // null pointers (nullptr) are implicitly converted to boolean value FALSE and pointers that hold addresses to TRUE
  if (ptr) std::cout << "The pointer, the pointer is real";
  return 0;
}
  • Dereferencing a null pointer also results in undefined behaviour.

19.3. Pointer to a Const

  • You can not assign a non-const pointer to a const variable.
const int age {5};
int* agePtr {&age}; // compile error!!

const int* agePtr2 {&age}; // will work!
agePtr2 = {&someOtherVariable}; // will still work!
  • A pointer to a const is not a const itself, so you can change the address it is pointing to.
  • A pointer to a const, when pointing to a non-const variable will not allow you to modify the value at the address it is pointing to.

19.4. Const Pointers

  • This is different from a pointer to a const.
  • A const pointer is a pointer whose address can not be changed once initialized.
int age {5};
int* const agePtr {&age};

agePtr = {&someOtherVariable}; // NOT ALLOWED

19.5. Const Pointer to a Const Variable

  • The combination of both the above!
int age {5};
const int* const agePtr {&age};
  • Just like normal pointer to const variables, the referents don't have to be const.

19.6. Passing Values to Functions By Address

#include <iostream>
void foo(int* num) {
  std::cout << *num << std::endl;
}

int main() {
  int age {5};
  foo(&age);
  return 0;
}

Prefer using references instead of pointers wherever possible.

  • Passing by address copies the address from the caller to the function.

19.7. Setting Optional Parameters in Functions

  • We can use nullptr to set optional parameters in functions.
void foo(const int* bar=nullptr) {
  if(bar) std::cout << *bar;
  else std::cout << "No";
}
  • Function overloading is a better way to achive the same result.
void foo(const int& bar) {
  std::cout << bar;
}

void foo() {
  std::cout << "No";
}

19.8. Top Level and Low Level Consts

  • Consts that apply to the object itself are called top level consts. For example, const int x {5};.
  • Consts that apply to the reference or pointer to the object are called low level consts. For example, int const* ptr {&x};.
int x {10};
const int* const y {&z}; // the left const is low level, the right const is top level

19.9. Auto and Pointers

19.9.1. TODO Read chapter 12.14 again!

  • When using the auto keyword, the pointer type is not dropped, only top level consts are dropped.
std::string s{};
const std::string* const ptr {&s};

// statement // final type deduced
auto ptr1 {ptr}; // const std::string*
auto* ptr2 {ptr}; // const std::string*

const auto ptr3 {ptr}; // const std::string* const
auto const ptr4 {ptr}; // const std::string* const

auto* const ptr5 {ptr}; // const std::string* const
const auto* ptr6 {ptr}; // const std::string*

const auto const ptr7 {ptr}; // error, const is used twice
const auto* const ptr8 {ptr}; // const std::string* const

20. User Defined Types

These include

  1. Enumerated Types
  2. Class Types
    1. Structs
    2. Classes
    3. Unions

Compilers need to see the full definition of user defined types. So, try to make a separate header file with the same name as the type and include it wherever you need to use it.

The C++ language standard defines a user-defined type as any class or enumerated type defined by you, the standard library or the implementation. This means that even std::string is considered a user defined type.

20.1. Enumeration Types

20.1.1. Unscoped Enumerations

  • They are defined using the enum keyword.
enum Color {
  red = 5, // assigned 5
  green, // assigned 6
  blue, // assigned 7
};

int main() {
  // Now, we can use Color as a data type
  Color apple {red};
  Color shirt {blue};

  Color ball {2}; // ERROR because 2 is not a part of the enum Color
  return 0;
}
namespace Color {
  enum Color {
    red, // assigned 0
    green, // assigned 1
    blue, // assigned 2
  }
}

int main() {
  Color::Color apple {Color::red}; // notice the data type and the enumerator
  return 0;
}
  1. Enumeration Size or Underlying Type
    • This is the specific type used to represent the integral values held by the enumerators.
    • Default type depends on the compiler but is mostly int.
    • You might want to use a different type to save bandwidth or any other reason-
    enum Color : std::int8_t {
      red,
      green,
      blue,
    }
    
    • Do this only when necessary.
    • If we specify the type like this, we can list initialize the variable using integer literals.
    enum Color : int {
      red,
      green,
      blue,
    };
    
    int main() {
      Color apple {0}; // okay, list initialization is allowed if type is specified.
      Color shirt (0); // compile error, copy initialization not allowed with integer literals
      return 0;
    }
    

20.1.2. Scoped Enumerations

  • Similar to unscoped but they prevent the following two things-
    1. They don't implicitly convert to integers
    2. They are only placed in the scope region of the enumeration
enum class Color {
  red,
  green,
  blue
};

int main() {
  Color apple {Color::red};
  Color shirt {Color::blue};
  return 0;
}
  • Since they don't implicitly convert to integers, you can not print them onto the screen using std::cout. You can use C++23's std::to_underlying(Color::red) or a normal static conversion to print them.
  • From C++20 onwards, we can do the following-
enum class Color {
  red,
  green,
  blue,
}

int main() {
  using enum Color;
  Color car {red}; // no need to use the scope resolution operator ::
  return 0;
}

20.2. Class Types

20.2.1. Structs

  • These are program defined types.
struct Employee {
  int id {};
  int age {};
  double wage {};
};

int main() {
  // no need to use "struct Employee john" unlike C
  Employee john {};
  return 0;
}
  • The variables inside a struct are called data members.
  • Structs are of aggregate type meaning they store multiple data members (these include C-style arrays, structs, std::array, etc).
  • Structs require aggregate initialization like this-
Employee john {1, 24, 50000.0}; // direct list initialization (preferred)
Employee doe = {2, 24, 60000.0}; // copy list initialization
  • If we leave the list empty (Employee john {};) when initializing a struct type, it will get value initialized (0 for integral types, "" for string etc..).

An type is NOT considered aggregate if it has any of the following-

  • User declared constructor
  • Private or protected non-static data members
  • Virtual functions
  1. Designated Initializers
    • Only for C++20 and above.
    #include <iostream>
    struct Employee {
      int id {};
      int age {};
      double wage {};
    };
    
    int main() {
    
      Employee tenma { .id{1}, .age{24}, .wage{60000.0}}; // ok
      Employee johan { .id{2}, .wage{60000.0}}; // ok, age is value initialized (0)
      Employee anna { .wage{60000.0}, .id{3}}; // error, order does not match
      std::cout << john.wage << std::endl;
    
      return 0;
    }
    

    Do not use designated initializers to prevent clutter. Use the normal way and if you need to add new data members, add them to the end so order won't mix up.

  2. Designated Assignment
    #include <iostream>
    struct Employee {
      int id {};
      int age {};
      double wage {};
    };
    
    int main() {
    
      Employee tenma{};
      tenma = {.id = 1, .age = 32, .wage = 50000.0};
      std::cout << john.wage << std::endl;
    
      return 0;
    }
    
  3. Default Member Initialization
    #include <iostream>
    struct Something {
      int first; // bad
      int second {}; // 0 initialized
      int third {5}; // default value 5
    };
    
    int main() {
      Something thing {}; // first is value initialized to 0, second has default member 0, third is 5
      std::cout << thing.first << std::endl;
    
      Something anotherThing; // not initialized (bad)
      std::cout << anotherThing.first << std::endl; // garbage value since it's not value initialized
      return 0;
    }
    

    Always provide a default value to each member of structs.

  4. Passing/Returning Structs From/To Functions
    • Always try to pass structs by references or pointers to avoid making copies.
    • When passing by reference, you can access properties by using . like thing.first.
    • When passing by pointers, you have to access properties by using -> like thing->first.
    • When returning, it is not necessary to create a temporary object and then return it.
    struct Point {
      int x {0};
      int y {0};
      int z {0};
    };
    
    // It returns point with default initialized members
    Point test() {
      // You can also do something like this
      /*
       * return Point {}; 
       * return {1, 2, 3}; 
       */
      return {};
    }
    
  5. Nested Structs
    #include <iostream>
    #include <string>
    
    struct Company {
      struct CEO {
        int id {};
        int age {};
      };
      int numberOfEmployees {};
      CEO companyCEO {};
    };
    
    int main() {
      Company myCompany {50000, {0, 20}};
      std::cout << myCompany.companyCEO.age << std::endl;
      return 0;
    }
    
  6. Size of Structs and Padding
    • The size of a struct is not always the sum of sizes of its data members.
    • The compiler can add certain gaps (called padding) in the struct for performance reasons. For example, if the sum of size is 14 bytes, it will add gaps such that the size of the actual struct will be 16 bytes (2x8).
    // size of this struct is 12
    struct One {
      short a {}; // 2 bytes of padding added to make the size 4 bytes
      int b {};
      short c {}; // 2 bytes of more padding added
    };
    
    // size of this struct is 8
    struct Two {
      int b {}; // no padding
      short c {}; // no padding
      short a {}; // no padding
    };
    

    Always define data members of structs in decreasing order of their size to avoid padding as much as possible.

20.2.2. Class Templates on Structs

  • Just like function templates are used to generate function definitions, class templates are used to generate class types (structs, classes, etc).
#include <iostream>

template <typename T>
struct Point {
  T x {};
  T y {};
};

int main() {
  Point<int> coords {0,0};
  std::cout << coords.x << std::endl;
  std::cout << coords.y << std::endl;
  return 0;
}
  • We can not overload structs for different data types. The compiler will see them just like a redefinition of the struct.
  • We can make functions for these template classes like this-
#include <iostream>
#include <cmath>

template <typename T>
struct Point {
  T x {};
  T y {};
};

template <typename T>
T getDistanceFromOrigin(Point<T> pt) {
  return (std::sqrt(std::pow(pt.x, 2) + std::pow(pt.y, 2)));
}

int main() {
  Point<int> a {2, 10};

  // template argument will be deduced automatically
  std::cout << getDistanceFromOrigin(a) << std::endl;
  return 0;
}
  1. std::pair
    • Working with pairs of data is very common.
    • The C++ standard library has a built-in class template for a pair type with first and second as the data members.
    #include <iostream>
    #include <utility>
    
    int main() {
      std::pair<int, double> a{2, 3.4};
      std::cout << a.first << std::endl;
      std::cout << a.second << std::endl;
      return 0;
    }
    
    • Favour using this instead of writing your own "pair" class template.
  2. CTAD (C++17)
    • Class Template Argument Deduction
    • Starting from C++17, the compiler can deduce the types of template arguments on its own when instantiating objects of the class templates.
    template <typename T>
    struct Point {
      T x;
      T y;
    };
    
    int main() {
      Point a {1, 2};
      return 0;
    }
    
    • The above program won't work with C++17 but will work on C++20 because prior to C++20, you had to specify a "deduction guide" to the compiler so that it knows how to deduce the type.
    template <typename T>
    struct Point {
      T x;
      T y;
    };
    
    // Deduction guide for C++17
    // We are simply saying that if it finds a declaration of Point with two arguments of type T and T
    // it should deduce the type to be of Pair<T>
    template <typename T>
    Point(T, T) -> Point<T>;
    
    int main() {
      Point a {1, 2};
      return 0;
    }
    

21. Libraries

21.1. Static Libraries

  • Also known as "archives".
  • These are compiled and linked directly to our program.
  • On Windows, they typically have extension .lib and on Unix they have extension .a.
  • One downside is that each executable using a static library has its own version of it, causing wasted space.

21.2. Dynamic Libraries

  • These become part of your program at runtime!
  • Many programs can use the same library.
  • They remain separate from the compiled binary.
  • On Windows, they have extension .dll and on Linux they have extension .so (shared object).

22. Object Oriented Programming

22.1. Classes

22.1.1. Introduction

  • A class is a program-defined type which can have member variables of different types and member functions.
  • Example definition of a class with member variables-
  • Structs and classes are almost identical in C++. Any example written using classes can also be written using structs and vice-versa.
  • Structs have all the capabilities that classes have in C++ (not in C though), so you may use the one you like or go with the convention of using classes.
class Employee {
  int id {};
  int age {};
  double wage {};
}

22.1.2. Member Functions

  • Functions defined inside class types are called member functions.
#include <iostream>
struct Date {
  int day {};
  int month {};
  int year {};

  void print() {
    std::cout << day << '/' << month << '/' << year << std::endl;
  }
};

int main() {
  Date today{23,3,2024};
  today.print();
  return 0;
}
  • Member functions can be written in any order in a class type.
  • Member functions can also be overloaded as long as they can be differentiated.
  • Defining members functions inside structs should be avoided because it makes them a non-agregate, and that's why we use classes instead.

22.1.3. Const Class Type Objects

  • Class type objects can be initialized with the const keyword.
  • They can not be left uninitialized.
  • No attempt to modify their properties is entertained.
  • Const objects are also not allowed to call non-const member functions-
#include <iostream>
struct Test {
  int id {};
  void print() {
    std::cout << "test" << std::endl;
  }
  void print2() const {
    std::cout << "test2" << std::endl;
    ++id; // error, const member functions can not modify members
  }
};

int main() {
  const Test t {0};
  t.id = 1; // error
  t.print(); // error again
  t.print2(); // ok

  return 0;
}

A function which does not modify the state of an object should absolutely be made const as a good practice.

  • You can overload the same member function with the exact same definition by adding a variant without the const keyword and one with the const keyword.

22.1.4. Access Levels

C++ classes have three access levels: public, private and protected.

  • Members of struct are public by default.
  • Members of class are private by default.
  • We can not use aggregate initialization for classes because their members are private by default!
  1. Public

    Public members of a class type can be accessed by anyone in the scope. We can set access level of data members using the access specifier public:.

    2004-6-5
    
  2. Private

    Private members of a class type can only be accessed by other members of the same class.

    It is a good practice to prefix private data members with an m_ like m_age.

    We can set the access level using private:

    #include <iostream>
    class Date {
    public:
      void print() const {
        std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
      }
    private:
      int m_year {2004};
      int m_month {6};
      int m_day {5};
    };
    
    
    
    int main() {
      Date date {};
      date.print();
      return 0;
    }
    

    Best Practice: Classes should generally make member variables private and member functions public. Structs should avoid using access specifiers and all members should be made public.

    • Private members can be accessed by other OBJECTS of the same class too!
  3. Access Functions
    • These are trivial public member functions whose job is to retreive or change the value of a private member variable.
    • These come in two flavours: getters and setters.
    • Getters are usually made const so they can be called on both const and non-const objects.
    • Setters should be non-const.
    #include <iostream>
    
    class Date {
    private:
      int m_year {};
      int m_month {};
      int m_day {};
    public:
      void print() {
        std::cout << m_year << '/' << m_month << '/' << m_day << std::endl;
      }
    
      int getYear() const { return m_year; }
      void setYear(int year) { m_year = year; }
      int getMonth() const { return m_month; }
      void setMonth(int month) { m_month = month; }
      int getDay() const { return m_day; }
      void setDay(int day) { m_day = day; }
    };
    

    A member function returning a reference should return a reference of the same type as the data member being returned, to avoid unnecessary conversions. For example, you should not return std::string_view for an std::string because that would require an unnecessary conversion.

22.1.5. Encapsulation (Data Hiding)

  • The interface of a class type defines how a user interacts with objects of that class type.
  • An interace composed of public members is sometimes called the public interace.
  • The implementation consists of the code that actually makes the class behave as intended. It includes the member variables which store data and the body of member functions.
  • Data hiding is also known as encapsulation.
  • It is a technique used to enforce the separation of interface and implementation.
  • It is simply implemented by making member variables private and providing public member functions to manipulate the private members.
  • Class invariants are conditions which must be true throughout the lifetime of an object in order for the object to stay in a valid state.
#include <iostream>

// consider this example
// the user will have to change the initial if he/she ever changes the name
// this is called a class invariant (initial must be the first letter of name throughout the program)
class Person {
public:
  std::string name {"John"};
  char initial {'J'};
};
  • This is one of the biggest reasons why we should use encapsulation.
  • Data hiding helps provide better error handling by verifying values in the setters.
  • It also helps us to change the implementation of the underlying member variables without breaking programs using the interface.

If a function can be implemented as a non-member function, consider implementing it as a non-member function. This will make sure-

  • Interface of your class is smaller.
  • They are easier to debug.

Best Practice- Declare public members first in your class definitions followed by private members. This is because any user looking at your class is only interested in the interface.

22.1.6. Constructors

  • Non-aggregate types (such as structs with private members or class types) can not be initialized using aggregate initialization.
  • One of the reasons is that, for data hiding, we don't want the user to worry about the implementation of the data members.
  • So, if the user had to initialize using aggregate initialization, then the user would need to know the order in which members were defined.
  • Constructors are functions used to initialize data members and do optional things like opening databases etc upon the creation of a class type object.
  • First, the object is created and then the matching constructor is called. If the constructor function is not found, an error is raised by the compiler.
  • Constructors can also be overloaded since they're just functions.
  • One class type can only have one default constructor (a constructor which does not take any parameters).
  1. Naming Constructors
    • Constructors have special rules which must be followed while naming them.
      1. The function must have the same name as that of the class type (with the same case).
      2. The function must not return anything.
    #include <iostream>
    #include <string>
    #include <string_view>
    
    class Student {
    private:
      std::string m_name {};
      int m_age {};
    
    public:
      // this is the constructor function without any return type
      Student (std::string_view name, int age) {
        m_name = name;
        m_age = age;
      }
    
      const std::string& getName() const {return m_name;}
      void setName(std::string_view name) {m_name = name;}
    
      int getAge() {return m_age;}
      void setAge(int age) {m_age = age;}
    };
    
    int main() {
      Student s1{"Prayag Jain", 20};
      std::cout << s1.getName() << std::endl;
      return 0;
    }
    
  2. Member Initialization List
    • No need to assign member variables to the arguments passed to the constructor.
    #include <iostream>
    #include <string>
    
    class Student {
    private:
      std::string m_name{};
      int m_age{};
    public:
      Student(std::string_view name, int age): m_name {name}, m_age {age} {
        std::cout << "Initialized successfully" << std::endl;
      }
    
      const std::string& getName() const {return m_name;}
      void setName(std::string_view name) {m_name = name;}
    
      int getAge() {return m_age;}
      void setAge(int age) {m_age = age;}
    };
    
    int main() {
      Student s1{"Prayag Jain", 20};
      std::cout << s1.getName() << std::endl;
      return 0;
    }
    
  3. Implicit Default Constructors
    • If no constructor is provided to a class, an implicit default constructor is created which can be used to initialize objects of the constructor.
    • The implicit constructor does not take any arguments and is equivalen to-
    class Student {
      private:
        int m_age{};
        char m_initial {}; 
      public:
        Student() {};
    };
    
  4. Explicit Default Constructors
    • In cases when we want to generate a default constructor similar to the implicit one, we can do something like this-
    class Student {
      private:
        int m_age{};
        char m_initial {}; 
      public:
        // explicit default constructor (prefer doing this)
        Student() = default;
        // overloaded constructor
        Student(int age, char initial) : m_age{age}, m_initial{initial} {};
    };
    
  5. Delegating Constructors
    • In a class type, constructors are allowed to transfer the ability to initialize to another constructor of the same class type.
    #include <iostream>
    #include <string>
    
    class Employee {
    private:
      int m_id {};
      std::string m_name {};
    
    public:
      Employee(int id, std::string name) : m_id{id}, m_name{name} {
        std::cout << "initialized" << std::endl;
      }
    
      // delegated to the above function
      Employee(std::string name) : Employee{0, name} {}
    };
    
    int main() {
      Employee e1("Prayag");
      Employee e2(1, "Parin");
      return 0;
    }
    
  6. Temporary Class Objects
    • We can temporarily create class objects to pass them to functions or use in expressions.
    • This is useful when you only want to use that object once.
    // assume a Ball object exists
    // ...
    // assume a print function exists
    // ...
    
    int main() {
      // temporary ball object passed to function
      print(Ball{"white", 5.0});
    
      // implicit conversion to ball object
      print({"white", 5.0});
    }
    
  7. Copy Constructor
    • Used to initialize an object with an object of the same type.
    • Implicit copy constructors are created when one is not created explicitly.
    • You can explicitly create a copy constructor like this-
    #include <iostream>
    #include <string>
    
    class Employee {
    private:
      std::string m_name {};
      int m_id{};
    public:
      Employee (const std::string name, const int id) : m_name(name), m_id(id) {
        std::cout << "constructor called" << std::endl;
      };
    
      // Explicit copy constructor
      Employee (const Employee& e) : m_name(e.m_name), m_id(e.m_id) {
        std::cout << "copy constructor called" << std::endl;
      };
    };
    
    int main() {
      Employee alice {"Alice", 1};
      Employee bob {alice}; // Calls the copy constructor
      return 0;
    }
    

    Prefer the implicit copy constructor, but if you do want to create an explicit one, the parameter must be a const lvalue reference. This is because if its passed by value, it would start an infinite chain of calls to the copy constructor (copy constructor is called when passing objects by value).

    • We can also tell the compiler to explicitly create a copy constructor for us-
    class Employee {
    private:
      std::string m_name {};
      int m_id{};
    public:
      Employee (const std::string name, const int id) : m_name(name), m_id(id) {
        std::cout << "constructor called" << std::endl;
      };
    
      // Explicit copy constructor
      Employee (const Employee& e) = default;
    };
    
    • We can prevent objects from being copied by using the = delete syntax.
    class Employee {
    private:
      std::string m_name {};
      int m_id{};
    public:
      Employee (const std::string name, const int id) : m_name(name), m_id(id) {
        std::cout << "constructor called" << std::endl;
      };
    
      // Copying the object will not be allowed
      Employee (const Employee& e) = delete;
    };
    
  8. Copy Elision
    • It is an optimization technique used by compilers that allows them to remove unnecessary copying of objects.
    • When the compiler optimizes away a call to the copy constructor, we say the object has been elided.
    • The compiler is free to remove unnecessary calls to the copy constructor, even if it has side effects!
    • This is one of the biggest reasons copy constructors should not have any side effects.
  9. Converting Constructors
    • All constructors are converting constructors by default.
    • Constructors are used to determine how to do implicit conversion where a specific class type object was required but something else was provided.
    • Only one user-defined conversion may be applied to perform an implicit conversion.
    #include <iostream>
    #include <string>
    #include <string_view>
    
    class Employee {
    private:
      std::string m_name {};
      int m_id {};
    
    public:
      Employee(const std::string_view name, int id) : m_name(name), m_id(id) {};
      Employee (const std::string_view name) : Employee(name, 0) {};
    
      Employee (const Employee& p) = default;
    
      const std::string& getName() const {return m_name;};
    };
    
    void getName(const Employee& e) {
      std::cout << e.getName() << std::endl;
    }
    
    int main() {
      getName("Parin"); // compiler error because "Parin" will first be converted to std::string_view from const char* and that will then be converted to an Employee{} object but only one implicit conversion is allowed (2 here).
      return 0;
    }
    
    • The explicit keyword can be used to specify that a constructor should not be used as a converting constuctor, hence, disallowing implicit conversions.
    class Employee {
    private:
      std::string m_name {};
      int m_id {};
    
    public:
    
      // implicit conversions won't be allowed
      explicit Employee(const std::string_view name, int id) : m_name(name), m_id(id) {};
      explicit Employee (const std::string_view name) : Employee(name, 0) {};
    
      Employee (const Employee& p) = default;
    
      const std::string& getName() const {return m_name;};
    };
    

    Make any constructor that accepts a single argument explicit by default. If an implicit conversion between types is both semantically equivalent and performant, you can consider making the constructor non-explicit.

    Do not make "copy" or "move" constructors explicit, as these do not perform conversions.

23. More on Classes

23.1. The this keyword

  • Every member function has an access to a const pointer named this.
  • This pointer points to the address of the current object.
#include <iostream>

class Point {
private:
  int m_x {};
  int m_y {};
public:
  Point(int x = 0, int y = 0) : m_x(x), m_y(y) {}
  int getX() const { return this->m_x; }
};

int main() {
  Point point{};
  std::cout << point.getX() << std::endl;
  return 0;
}

23.2. Returning *this

  • We can return *this keyword in member functions and achieve function chaining!
#include <iostream>

class Fraction {
private:
  int m_numerator {};
  int m_denominator {};
public:
  Fraction() = default;
  Fraction(int n, int d) : m_numerator(n), m_denominator(d) {};

  void print() const {
    std::cout << m_numerator << "/" << m_denominator << std::endl;
  }

  Fraction& add(const Fraction& f) {
    m_numerator = m_numerator * f.m_denominator + m_denominator * f.m_numerator;
    m_denominator *= f.m_denominator;
    return *this;
  }
  Fraction& multiply(const Fraction& f) {
    m_numerator *= f.m_numerator;
    m_denominator *= f.m_denominator;
    return *this;
  }
  Fraction& subtract(const Fraction& f) {
    m_numerator = m_numerator * f.m_denominator - m_denominator * f.m_numerator;
    m_denominator *= f.m_denominator;
    return *this;
  }
};

int main() {
  Fraction f1{2, 5};
  f1.add(Fraction{3, 8}).multiply(Fraction{2, 3}).print();
  return 0;
}

23.3. Resetting Class Objects

  • We can do something like this to reset class type objects back to their default state-
class Point {
private:
  int m_x{};
  int m_y{};
public:
  // ...
  void reset() {
    // reset the point object
    *this = Point{};
  }
};

23.4. Classes and Header Files

  • Compilers need to see the full definition of class types in order to use them.
  • There's no "forward declaration" for class types.
  • Member functions CAN be forward declared like this-
class Test {
public:
  void test() const;
};

void Test::test() const {
  // something
  int x;
}

int main() {
  return 0;
}

23.5. Nested Types

  • Class types can have nested types like this-
class Fruit {
  public:
    enum Type {
      orange, apple, banana
    };
  private:
    Type m_type {orange};
    bool m_eaten {false};
  public:
    Fruit() = default;
    Fruit(Type t, bool eaten) : m_type{t}, m_eaten{eaten} {};
};
  • The enum Type has been made public and can be accessed via Fruit::Type and its enumerators can be accessed like this- Fruit:apple.
  • We've used enum instead of enum class because classes have their own scope so we don't need to worry about polluting the global scope.
  • You can also use typedef and type aliases in a similar way to the above.

23.6. Destructors

  • When an object of a non-aggregated class type is destroyed, a special member function called the destructor is called automatically before hand.
  • Destructors, just like constructors, have special rules-
    1. It should have the same name as that of the class but prefixed with a tilde ~.
    2. It cannot take any arguments.
    3. It cannot return anything.
#include <iostream>

class Test {
public:
  Test() {
    std::cout << "Created object" << std::endl;
  }
  ~Test() {
    std::cout << "Destroyed object" << std::endl;
  }
};

int main() {
  Test t;
  return 0;
}

23.7. Class Templates with Member Functions

#include <iostream>

template <typename T>
class Pair {
    private:
        T m_first {};
        T m_second {};
    public:
        Pair(T first, T second) : m_first{first}, m_second{second} {};
};

int main() {
    Pair<int> p {1, 2};
    return 0;
}
  • Since T might be expensive to copy, it's recommended to use const T& for the constructor parameters.

23.8. Static Members

  • Static members of a class type in C++ are shared across all the objects instantiated from that class type.
#include <iostream>

struct Test {
    static int test;
};

// initialize test to 1
// because static members exist independently of the objects instantiated from the class type,
// they can be accessed anytime using the scope resolution operator
int Test::test{1};

int main() {
    Test foo{};
    Test bar{};

    foo.test = 2;

    std::cout << bar.test << std::endl;
    return 0;
}
  • Static members can be accessed even before any object is initialized.
  • When we declare a static member variable inside a class, it's like forward declaring it (but different).
  • You must explicitly define the global variables outside the class just like we're doing in the above example.
  • You should not instantiate static members inside header files, only put them in .cpp files, because if the header is included more than once, you'll get multiple definitions of the same variable (violation of ODR, compile error).

You can also initialize static members inside the class definition itself but only if it's one of the following:

  1. It is a const variable
  2. It is a constexpr variable
  3. It is an inline variable

23.9. Static Member Functions

  • Member functions can also be defined as static. They can be used to make access functions for private member variables.
#include <iostream>
class Test {
  private:
    static const int m_id{10};
  public:
    static int getID() {return m_id;};
    static void test();
};

// defining static functions outside
// no need to specify "static" here
void Test::test() {
    std::cout << "hey" << std::endl;
}

int main() {
    // the function is being accessed without
    std::cout << Test::getID() << std::endl;
    Test::test();
    return 0;
}
  • Static member functions do not have a *this pointer because they're not associated with any object.

23.10. Friend Functions

  • A friend function is a function (may it be a member or a non-member function) that can access all private and protected members of a class as if it was a member of that class.
#include <iostream>

class Point {
    private:
        int m_x {};
        int m_y {};
    public:
        Point (int x, int y) : m_x{x}, m_y{y} {};

        friend void print(const Point& p);
};

// the outsider function which is considered a "friend"
void print(const Point& p) {
    std::cout << p.m_x << "," << p.m_y << std::endl;
}

int main() {
    Point point{4,5};
    print(point);
    return 0;
}
  • You should prefer using non-friend functions over friend functions.
  • Friend functions don't have a *this pointer.

23.11. Friend Class

  • Similar to friend functions, a friend class can access private and protected members of another class.
#include <iostream>

class One {
    private:
        int m_x {5};
    public:
        One() = default;

        // making friends with Two :)
        friend class Two;
};

// the friend class
class Two {
    public:
        static void test(const One& o) {
            std::cout << o.m_x << std::endl;
        }
};

int main() {
    One obj{};

    Two::test(obj);
    return 0;
}
  • If class A is a friend of B, it doesn't mean that class B is also a friend of A!

23.12. Ref Qualifiers

  • When returning values by reference or by value from member functions, we can get stuck in a difficult situation-
    1. If the function returns by value (rvalue), it can be used safely anywhere but might be inefficient to copy.
    2. If the function returns by reference (lvalue), it's faster but can result in undefined behaviour (if the obj is destroyed).
  • We can overload functions in C++ classes to specify different behaviour based on the context (one where implicit object is an lvalue and another one in which the implicit object is an rvalue).
//  & qualifier overloads function to match only lvalue implicit objects, returns by reference
const auto& getName() const & { return m_name; }

// && qualifier overloads function to match only rvalue implicit objects, returns by value
auto getName() const && { return m_name; }

Use of ref qualifiers is not recommended for a few reasons-

  1. Most developers don't know about this so this can cause errors or inefficiencies in use.
  2. Use of this can be prevented easily by using good practices.

24. Vectors

  • std::vector is one of the container classes in C++ that implements an array.
#include <vector>

int main() {

    // specifying the type
    std::vector<int> nums {1,2,3,4};

    // type deduction based on CTAD (preferred)
    std::vector vowels {'a', 'e', 'i', 'o', 'u'};
    return 0;
}
  • The container classes use a special type of constructor called the "list constructor" to initialize the elements.
  • Passing an invalid index to operator[] to access elements will cause undefined behaviour since this operator does not do any checking.

24.1. Constructing arrays of specific length

  • std::vector has a constructor for this- explicit std::vector<T>(int)
std::vector<int> zeroes(10); // this will create an array of 10 zeroes (value initialized)
  • This constructor must be called using direct initialization and not direct-list initialization.

24.2. const and constexpr vectors

  • Vectors can be made const, and such vectors can't be modified later.
  • const vectors must be initialized.
  • Non-const vectors can NOT contain const values so something like this won't compile: std::vector<const int> nums {1,2,3,4};
  • Vectors can not be made constexpr. You can use std::array for that.

24.3. Size of a vector

  • You can get the number of elements in a vector by using the size() member function of the vector object.
#include <vector>
#include <iostream>

int main() {
    std::vector nums {1,2,3,4,5};
    std::cout << nums.size() << std::endl;
    return 0;
}
  • You can also use the non-member function std::size to get the size of any type of array: std::size(myArr).
  • C++20 introduced a non-member std::ssize function which returns the size of an array as a signed type.

24.4. Accessing elements of a vector

  • When we use operator[] to access elements of an array, there's no bound checking. Accessing non existing indices leads to undefined behaviour.
  • We can (but should not) use the .at() member function to access elements with runtime bounds checking.
  • We can use constexpr int variables to access elements without getting narrowing conversion warnings (because std::size_t is required).
  • We should use std::size_t for iterations and accessing elements but this is an unsigned type and might not always be desirable..
  • If we want, we can use int or std::ptrdiff_t (when the number of elements is large) to access elements.

24.5. Passing Vectors

  • When passing vectors to functions, their full type must be speficied-
void foo(const std::vector<int>& myVector);
  • We prefer passing them by reference.

24.6. Move Semantics

  • These are the rules which determine how the data from one object is moved to another object.
  • Moving data, i.e., transferring ownership, to other objects is much faster than copying it.
  • When move semantics is invoked, any data that can be moved is moved while others are copied.
  • std::string and std::vector, both support move semantics, so they can safely be returned by value in functions.
  • Move semantics are invoked automatically for supported types.

24.7. Range Based For Loop

#include <iostream>
#include <vector>
int main() {
    std::vector nums {1,2,3,4,5}; // CTAD to deduce type of template parameter

    for (const auto& num : nums) {
        std::cout << num << std::endl;
    }
    return 0;
}
  • We should use the auto keyword to deduce the type in the loop.

24.8. Resizing Vectors

  • Vectors are dynamic arrays.
  • They can be resized using the resize() member function.
std::vector nums {1,2,3}; // 3 elements
nums.resize(5); // now stores 5 elements
  • We can get the capacity of a vector, which is the number of elements it can hold using the capacity() member function.
  • Resizing works by first acquiring new memory space, copying existing elements and deleting the old memory.
  • The above is the reason that resizing is expensive!

Resizing a vector to be smaller will only decrease its length, and not its capacity.

24.9. Stacks

  • Just like stacks of pizzas and books.
  • You can add/remove items only from the top.
  • Order of insertion/deletion is last-in, first-out (LIFO).
  • The last plate added onto the stack will be the first plate that is removed.

24.9.1. The Stack Interface

  • Vectors can behave like stacks.
  • There are some built-in member functions in vectors
    1. push_back(): push elements
    2. pop_back(): pop elements
    3. back(): peek the top element
    4. emplace_back(): push elements (more efficient when pushing objects)
  • The push_back and pop_back functions will increment the length of the vector, and reallocate memory if necessary.
  • The resize method changes the capacity as well as the length.
  • To increase only the capacity (but not the length) we can use the reserve() method.

Use emplace_back() when you need to push temporary objects of class types or anything to a vector and use push_back() when you need to push anything else to a vector.

25. Fixed Size Arrays

  • The primary reason why we should not use std::vector everywhere is that they can't be made constexpr.
  • constexpr arrays are much more performant.

25.1. std::array

#include <array>
#include <vector>

int main() {
    constexpr int len {5};
    std::array<int, len> myArr{}; // array with 5 elements (direct list initialization)
    std::array<int, 5> myArr2 = {}; // copy list initialization

    const std::array<int, 5> myArr3 {1,2,3,4,5}; // all elements are const (even though not specified in the template argument)

    std::vector<int> myArr4(5); // vector with 5 elements
    return 0;
}
  • Length of a standard array must be a constant expression. It can either be an integer literal, constexpr variable or an unscoped enum.
  • std::array is an aggregate type, meaning it has no constructors and it is initialized using aggregate initialization.
  • Don't forget that in some situations the compiler will treat const expressions as constexpr so const also works!

If you're using an std::array without marking it as constexpr consider using a vector instead lol

  • The second template argument is a non-type template argument of type std::size_t which must be constexpr.
  • Since this value must be constexpr, we can use any signed-type and the compiler will convert it without any conversion warnings!
  • We can use std::get non-member function to do compile-time bounds checking (unlike .at() member function which does runtime bounds checking).
#include <iostream>
#include <array>

int main() {
    constexpr std::array myNums {9,8,7,6,5}; // CTAD

    constexpr int index {3};
    std::cout << std::get<index>(myNums) << std::endl;
    return 0;
}

25.2. std::reference_wrapper

  • You can neither make an array nor a vector of references in C++.
  • Something like this won't work: std::vector<int&> {a, b, c};
  • This is applicable to all array types.
  • std::reference_wrapper is a class template which behaves like a modifiable lvalue reference to T (its template argument).
#include <iostream>
#include <functional> // for std::reference_wrapper
#include <array>

int main() {
    int x {1};
    int y {2};
    int z {3};

    std::array<std::reference_wrapper<int>, 3> arr {x, y, z};
    std::cout << arr[0].get() << std::endl; // we must use the get member function

    arr[0].get() = 5; // reassigning

    std::cout << arr[0].get() << std::endl; // we must use the get member function

    return 0;
}
  • We can use std::ref and std::cref as aliases to std::reference_wrapper and const std::reference_wrapper respectively.

25.3. Multi-Dimensional Arrays

  • We need to use double braces when initializing multi-dimensional arrays.
#include <iostream>
#include <array>

int main() {
    constexpr std::array<const std::array<int, 3>, 4> myArr {{
    {1, 2, 3},
    {1, 2, 3},
    {1, 2, 3},
    {1, 2, 3}}};

    for (const auto& row : myArr) {
        for (const auto& x : row)
            std::cout << x << " ";
        std::cout << std::endl;
    }
    return 0;
}
  • We can use alias templates to make our lives better!
#include <iostream>
#include <array>

template <typename T, std::size_t Rows, std::size_t Cols>
using Array2d = std::array<std::array<T, Cols>,Rows>;

int main() {

    Array2d myArr {{
            {1, 2, 3},
            {1, 2, 3},
            {1, 2, 3},
        }};
    return 0;
}

26. Iterators

  • An iterator is an object designed to traverse through a container class.
  • The following is a very basic example of an iterator.
#include <iostream>
#include <array>

int main() {
    constexpr std::array<int, 5> arr = {1, 2, 3, 4, 5};
    const int* ptr = &arr[0];
    const int* end = ptr + arr.size();

    while (ptr != end) {
        std::cout << *ptr << std::endl;
        ++ptr;
    }
    return 0;
}
  • The standard library provides member functions to get the beginning and end of a container class.
  • The begin() member function returns an iterator to the beginning of the container.
  • The end() member function returns an iterator to the end of the container.
  • The standard library also provides generic functions std::begin() and std::end() which can be used with C-style arrays.

27. Functions

27.1. Function Pointers

  • A function pointer is just a pointer that points to a function.
  • The syntax for declaring a function pointer is a bit tricky.

    #include <iostream>
    int sum(int a, int b) {
        return a+b;
    }
    
    
    int main() {
        int (*sumPtr)(int, int){&sum};
        std::cout << (*sumPtr)(1, 2) << std::endl;
        std::cout << sumPtr(1, 2) << std::endl; // implicit dereference
        return 0;
    }
    

Checkout clockwise/spiral rule or the right/left rule for understanding complex C/C++ declarations. https://c-faq.com/decl/spiral.anderson.html https://web.archive.org/web/20110818081319/http://ieng9.ucsd.edu/~cs30x/rt_lt.rule.html

  • Function parameters are resolved at compiled time while function pointers are resolved at runtime, therefore, default parameter won't work for function pointers
  • We can use type aliases to make function pointers look less scary.

27.1.1. Passing Function Pointers

#include <functional>
// instead of doing this
void myFunc(int(*funcPtr)(int, int)) {
    funcPtr();
}

using GetSum = int(*)(int, int);

// do this
void myFunc(GetSum funcPtr) {
    funcPtr();
}

// you can also do this (recommended)
void myFunc(std::function<int(int, int)> funcPtr) {
    funcPtr();
}

// or this
using GetSum = std::function<int(int, int)>

28. Stack and Heap

28.1. Segments

  • Memory used by a program is divided into these areas called segments:
    1. Code Segment: Where the compiled program sits in memory.
    2. BSS Segment: Where zero-initialized global and static variables are stored.
    3. Data Segment: Where initialized global and static variables are stored.
    4. Heap: Where dynamically allocated variables are allocated from.
    5. Call Stack: Where function parameters, local variables, and other function related information are stored.

28.2. Heap

  • Keeps track of dynamic memory allocation.
int* ptr {new int}; // stored in the heap
  • Allocating on a heap is slow.
  • Allocated memory stays in the heap until de-allocated or the application ends, which may cause memory leaks.
  • Allocated memory must be accessed through a pointer and pointer de-referencing is slower than using normal variables.
  • Heaps are larger than stacks (you can store large stuff in them).

28.3. Call Stack

  • When a function call is encountered, it is pushed onto the call stack.
  • When the current function ends, it is popped off the call stack.
  • Here, "it" is the stack frame, which holds information related to the function.
  • Allocating memory in the stack is fast.

28.3.1. Stack Overflow

  • The size of the call stack is limited.
  • When too many information is stored in a stack, it results in stack overflow.

29. Ellipses

  • Allows you to specify that a function can take in any number of arguments.
  • You should avoid using them.
  • They don't carry the count information (you have to manually pass it) and they don't have type information.
#include <iostream>
#include <cstdarg>

int sum(int count, ...) {
    int sm {0};
    std::va_list list;

    // first argument is the list, second is the last argument before the ellipsis
    va_start(list, count);

    for (int i {0}; i < count; ++i) {
        sm += va_arg(list, int);
    }

    va_end(list);

    return sm;
}

int main() {
    std::cout << sum(3, 1, 2, 3) << std::endl;
    std::cout << sum(5, 1, 2, 3, 4, 5) << std::endl;
    std::cout << sum(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;

    return 0;
}

30. Lambda Functions

  • Lambda functions are anonymous functions that can be used to create functions on the fly.
// Syntax
[ captureClause (optional) ] ( parameters (entirely optional) ) -> returnType (optional, auto is assumed) { body }
  • The following is a valid lambda function

    [] {}
    
  • Lambdas can only access global identifiers, entities known at compile time or entities with static storage duration.

30.1. Passing Lambda Functions

// Case 1: use a `std::function` parameter
void repeat1(int repetitions, const std::function<void(int)>& fn)
{
    for (int i{ 0 }; i < repetitions; ++i)
        fn(i);
}

// Case 2: use a function template with a type template parameter
template <typename T>
void repeat2(int repetitions, const T& fn)
{
    for (int i{ 0 }; i < repetitions; ++i)
        fn(i);
}

// Case 3: use the abbreviated function template syntax (C++20)
void repeat3(int repetitions, const auto& fn)
{
    for (int i{ 0 }; i < repetitions; ++i)
        fn(i);
}

// Case 4: use function pointer (only for lambda with no captures)
void repeat4(int repetitions, void (*fn)(int))
{
    for (int i{ 0 }; i < repetitions; ++i)
        fn(i);
}

30.2. The Capture Clause

  • The capture clause is used to give indirect access to the variables in the surrounding scope.
  • By indirect access, we mean that a copy of that variable is created for access. Even the data type may be different.
#include <iostream>

int main() {
    const char* name {"Prayag"};

    std::cout << [](){return name;}() << std::endl; // can't access name
    std::cout << [name](){return name;}() << std::endl; // okay
}
  • The variables accessed through captures are const and can not be modified.
  • We can mark the lambda mutable to allow modifying the captured variables.
#include <iostream>
#include <string>
int main() {
    using namespace std::string_literals;
    std::string name {"Prayag"s};
    [name]() mutable {
        // Will only modify the "copy" created after capturing
        name = "Parin";
        std::cout << name << std::endl;
    }();
    std::cout << name << std::endl;
    return 0;
}

30.3. Capture by Reference

  • Will reference to the original variable instead of a copy.
  • Non-const by default.
#include <iostream>
#include <string>

int main() {
    using namespace std::string_literals;
    std::string name {"Prayag"s};
    int age {20};

    // no need of "mutable" since these are non-const by default
    [&name, age]() {
        name = "Parin";
        std::cout << name << std::endl;
        std::cout << age << std::endl;
    }();

    std::cout << name << std::endl;
    std::cout << age << std::endl;
    return 0;
}

30.4. Default Capture

  • Capture all variables used in the lambda block automatically.
#include <iostream>
#include <string>

int main() {
    using namespace std::string_literals;
    std::string name {"Prayag"s};

    // default capture by value
    [=]() mutable {
        name = "Parin";
        std::cout << name << std::endl;
    }();

    std::cout << name << std::endl;

    // default capture by reference
    [&]() {
        name = "Parin";
        std::cout << name << std::endl;
    }();

    std::cout << name << std::endl;
    return 0;
}

30.5. Defining Variables in Capture Block

  • No need to specify the type of the variable in the capture block.
#include <iostream>
auto myFunc {[age{5}](){
    std::cout << "Age is " << age << std::endl;
}};
int main() {
    myFunc();
    return 0;
}

31. Operator Overloading

  • In C++, operators are implemented as functions. For example, x + y is equivalent to operator+(x, y).
  • Almost all operators can be overloaded in C++ to work with different data types except:
    1. Conditional: ?:
    2. sizeof
    3. Scope: ::
    4. Member selection: .
    5. Pointer member selector: .*
    6. typeid
    7. Casting operators
  • You can only overload operators that exist. You can not create your own operators.
  • To overload an operator, atleast one of the operands must be a user-defined type.

31.1. Overloading using friend functions

#include <iostream>
class Fraction {
    private:
        int m_numerator {0};
        int m_denominator {1};
    public:
        Fraction(int p, int q) : m_numerator{p}, m_denominator{q} {}

        // Overloading + operator
        friend Fraction operator+(const Fraction& a, const Fraction& b);

        void print() const {
            std::cout << m_numerator << "/" << m_denominator << std::endl;
        }
};

Fraction operator+(const Fraction& a, const Fraction& b) {
    int num {a.m_numerator*b.m_denominator+b.m_numerator*a.m_denominator};
    int den {a.m_denominator*b.m_denominator};
    return Fraction{num, den};
}

int main() {
    Fraction f1{4,5};
    Fraction f2{5,3};

    Fraction sum{f1+f2};
    sum.print();

    return 0;
}

31.2. Overloading using normal functions

#include <iostream>
class Fraction {
    private:
        int m_numerator {0};
        int m_denominator {1};
    public:
        Fraction(int p, int q) : m_numerator{p}, m_denominator{q} {}

        int p() const {
            return m_numerator;
        }

        int q() const {
            return m_denominator;
        }

        void print() const {
            std::cout << m_numerator << "/" << m_denominator << std::endl;
        }
};

// A normal function, accessing members using getters
Fraction operator+(const Fraction& a, const Fraction& b) {
    int num {a.p()*b.q() + b.p()*a.q()};
    int den {a.q()*b.q()};
    return Fraction{num, den};
}

int main() {
    Fraction f1{4,5};
    Fraction f2{5,3};

    Fraction sum{f1+f2};
    sum.print();

    return 0;
}
  • Prefer these over friend functions.

31.3. Overloading I/O Operators

  1. We can overload the << operator in the same way as above, only the left operand is fixed.
#include <iostream>

class Fraction {
    private:
        int m_p {0};
        int m_q {1};
    public:
        Fraction (int p, int q) : m_p (p), m_q(q) {};

        // This is a non-member function even though it is defined inside the class
        friend std::ostream& operator<<(std::ostream& out, const Fraction& frac) {
            out << frac.m_p << "/" << frac.m_q;
            return out;
        };

        friend std::istream& operator>>(std::istream& in, const Fraction& frac) {
            in >> frac.m_p;
            in >> frac.m_q;
        }
};

int main() {
    Fraction a{4,5};
    std::cout << "The fraction is " << a << std::endl;
    return 0;
}

31.4. Overloading using member functions

#include <iostream>
class Point {
    private:
        int m_x {};
        int m_y {};
    public:
        Point (int x, int y) : m_x{x}, m_y{y} {}

        Point operator+(Point b) const {
            return Point {b.m_x + m_x, b.m_y + m_y};
        };

};
int main() {
    Point a{3, 4};
    Point b {a + Point{1,2}};
    return 0;
}
  • You can not overload certain operators using member functions. For example, the << operator. This is because the overloaded operator must be added as a member of the left operand. Here, the left hand operand is std::ostream which is a part of the standard library and we can't modify it.

The following rules of thumb can help you determine which form is best for a given situation:

  1. If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function.
  2. If you’re overloading a unary operator, do so as a member function.
  3. If you’re overloading a binary operator that does not modify its left operand (e.g. operator+), do so as a normal function (preferred) or friend function.
  4. If you’re overloading a binary operator that modifies its left operand, but you can’t add members to the class definition of the left operand (e.g. operator<<, which has a left operand of type ostream), do so as a normal function (preferred) or friend function.
  5. If you’re overloading a binary operator that modifies its left operand (e.g. operator+=), and you can modify the definition of the left operand, do so as a member function.

31.5. Overloading unary operators

#include <iostream>
class Cents {
    private:
        int m_cents {0};
    public:
        Cents (int val) : m_cents{val} {};

        bool operator!() const {
            return m_cents == 0;
        }
};
int main() {
    std::cout << std::boolalpha; // causes bools to print as "true" or "false"
    std::cout << !Cents{0} << std::endl;
    return 0;
}

31.6. Overloading increment/decrement operators

  • Prefix -- and ++ operators are overloaded normally.
  • Postfix -- and ++ operators have an int parameter set (even though it is not used).
// int parameter means this is the postfix operator, for example, b++;
Digit Digit::operator++(int)
{
    // Create a temporary variable with our current digit
    Digit temp{*this};

    // Use prefix operator to increment this digit
    ++(*this); // apply operator

    // return temporary result
    return temp; // return saved state
}

31.7. Overloading the subscript operator

class IntList
{
private:
    int m_list[10]{};

public:
    int& operator[] (int index)
    {
        return m_list[index];
    }
};

31.8. Overloading the parenthesis operator

  • Unlike other operators, where the number of parameters is fixed, the parenthesis operator can accept any number of arguments.
  • This operator must be defined as a member function though.
#include <iostream>

class Matrix {
    private:
        int m_mat[3][3] {};
    public:
        // If the object is not const, the function can return an lvalue reference
        int& operator()(int row, int col) {
            return m_mat[row][col];
        }
        // If the object is const, the function must be const and it cannot return an lvalue reference
        int operator()(int row, int col) const {
            return m_mat[row][col];
        }

        void print() const {
            for (std::size_t row {0}; row < 3; row++) {
                for (std::size_t col {0}; col < 3; col++) {
                    std::cout << m_mat[row][col] << (col == 2 ? "" : ", ");
                }
                std::cout << std::endl;
            }
        }
};

int main() {
    Matrix a{};
    a(2,2) = 5;
    a(1,0) = 2;
    a.print();
}

31.9. Functors

  • We can implement "functors" by overloading the parenthesis operator.
  • Functors are just classes that operate like functions.
  • They hold the advantage of being able to store persistent data inside them because they are classes.
#include <iostream>
class Player {
    private:
        double m_money{0};
    public:
        void operator()(double i) {
            m_money += i;
        }
        double getMoney() const {
            return m_money;
        }
};

int main() {
    Player a {};
    a(10); // using it like a function call
    a(4.2);
    a(9.2);
    std::cout << a.getMoney();
    return 0;
}

31.10. Overloading typecasts

  • In many cases, we may need to allow the user to typecast our classes. For example, a "Cents" class may be typecasted into an int.
  • To overload the typecast operator, we don't specify the return type. For example-
#include <iostream>
class Cents {
    private:
        int m_cents {0};
    public:
        Cents(int x) : m_cents{x} {}

        // overloading typecasting to "int"
        operator int() const {
            return m_cents;
        }

        explicit operator double() const {
            return static_cast<double>(m_cents);
        }
};

int main() {
    Cents a {10};
    std::cout << static_cast<int>(a) << std::endl;
    std::cout << static_cast<double>(a) << std::endl;
}
  • When the compiler feels the need to do an implicit cast, it can do that using our overloaded method.
  • We can specify that we will only allow explicit typecasting, for example, using the static_cast operator.

31.11. Overloading the assignment operator

  • This must be implemented as a member function only.
  • We can return *this pointer from the overloaded function to add the ability to chain the assignment.
#include <iostream>

class Fraction {
    private:
        int m_num{0};
        int m_den{1};
    public:
        Fraction(int p=0, int q=1) : m_num{p}, m_den{q} {};

        friend std::ostream& operator<<(std::ostream& out, const Fraction& fr) {
            out << fr.m_num << "/" << fr.m_den;
            return out;
        }

        Fraction& operator= (const Fraction& fr) {
            // Checking for self assignment
            // This is not necessary in this case but still recommended!
            // It's necessary when self assignment can cause
            // problems like in the case of pointers
            if (this == &fr)
                return *this;

            m_num = fr.m_num;
            m_den = fr.m_den;

            return *this;
        }
};

int main() {
    Fraction a{4,5};
    Fraction b{5,6};
    Fraction c{};
    c = b = a;
    std::cout << c << std::endl;
    std::cout << b << std::endl;
    std::cout << a << std::endl;
    return 0;
}

31.12. Shallow and Deep Copy

31.12.1. Shallow Copy

  • C++ provides a default copy constructor along with a default assignment operator for every class.
  • This does not cause any problems for simple classes, but classes where dynamic memory allocation is involved, it might cause problems.
  • This is called shallow copy.
  • Look at this example for details: learncpp.com

31.12.2. Deep Copy

  • Deep copy is used for dynamic memory allocation.
// assumes m_data is initialized
void MyString::deepCopy(const MyString& source)
{
    // first we need to deallocate any value that this string is holding!
    delete[] m_data;

    // because m_length is not a pointer, we can shallow copy it
    m_length = source.m_length;

    // m_data is a pointer, so we need to *deep copy* it if it is non-null
    if (source.m_data)
    {
        // allocate memory for our copy
        m_data = new char[m_length];
        // do the copy
        for (int i{ 0 }; i < m_length; ++i)
            m_data[i] = source.m_data[i];
    }
    else
        m_data = nullptr;
}

// Copy constructor
MyString::MyString(const MyString& source)
{
    deepCopy(source);
}

32. Dynamic Memory Allocation

32.1. Using the new keyword

  • You can allocate memory to a pointer dynamically using the new keyword just like malloc() in C.
#include <iostream>

int main() {

  // create an uninitialized variable
  int* n {new int};
  *n = 5;

  // create an initialized variable
  int* m {new int{5}};
  std::cout << *n << std::endl;
  std::cout << *m << std::endl;

  // return memory to the operating system
  delete n;
  delete m;

  // make the pointer un-dangling
  n = nullptr;
  m = nullptr;

  return 0;
}
  • If, for some reason, the operating system fails to grant memory, an exception will be thrown.
  • We can tell new to return nullptr instead of throwing an error-

    int* n {new (std::nothrow) int{}};
    

32.2. Memory Leaks

  • Memory leaks are caused in two situations:
    1. When a program does not free the memory of a pointer after it terminates.
    2. When a pointer is reassigned to another address without deleting it first.

32.3. Arrays

#include <iostream>

int main() {
  std::size_t size{5};
  // std::cout >> "Enter size: ";
  // std::cin << size;

  int* array { new int[size] };

  for (std::size_t i {0}; i < size; i += 2) {
    array[i] = 6;
    array[i+1] = 9;
  }

  for (std::size_t i {0}; i < size; i++)
    std::cout << array[i] << std::endl;

  // free the memory!
  // You must use the [] to tell that there are multiple blocks to be cleaned
  delete[] array;
  return 0;
}
  • There's no built-in way to resize memory dynamically allocated to an array in C++. Use ~vector~s instead.

32.4. Void Pointers

  • Void pointers can be assigned to memory addresses holding objects of any data types.
  • Dereferencing void pointers is illegal.
  • You must cast the pointer to the actual data type before dereferencing.
  • The same goes when you want to delete a void pointer.
  • Pointer arithmetic is also not allowed on void pointers because it doesn't know the size of the memory blocks.
#include <iostream>

int main() {
  // random variable
  int num {5};

  // our void pointer
  void* ptr {&num};

  // casting it to the correct data type before dereferencing
  std::cout << *(static_cast<int*>(ptr)) << std::endl;
  return 0;
}

33. RAII

  • It is a programming technique, not specific to C++.
  • Full form is Resource Acquisition is Initialization.
  • It states that whenever a resource is acquired (memory, file, database handle, etc.) by an object, the resource is released as soon as the object is destroyed.
  • This is implemented using the concept of constructors and destructors in C++.
#include <iostream>

class Nums {
  private:
  int* m_arr{};
  int m_length{};

  public:
  // Constructor
  Nums(std::size_t length) {
    m_arr = new int[length];
    m_length = length;
  }

  // Destructor
  ~Nums() {
    delete m_arr[];
  }
}

int main() {
  return 0;
}

34. Smart Pointers

  • In C++, whenever a resource is acquired, it must be released before terminating the program.
  • When using pointers, it can be hard to do this manually everytime (you might forget about it sometimes).
  • Smart pointers can be implemented for this.
  • A smart pointer is just a class that manages ownership and destruction of another pointer.
#include <iostream>

template <typename T>
class SmartPtr {
private:
  T* ptr {nullptr};
public:
  SmartPtr(T* p = nullptr) : ptr {p} {}

  // copy constructor using move semantics
  SmartPtr(SmartPtr& src) {
    // transfering ownership
    std::cout << "transfering ownership" << std::endl;
    ptr = src.ptr;
    src.ptr = nullptr;
  }

  // Assignment operator
  SmartPtr& operator=(SmartPtr& src) {
    std::cout << "transfering ownership" << std::endl;
    if (this == &src) {
      return *this;
    }

    delete ptr;
    ptr = src.ptr;
    src.ptr = nullptr;
    return *this;
  }

  ~SmartPtr() {
    delete ptr;
  }

  T& operator*() const {
    return *ptr;
  }
  T* operator->() const {
    return ptr;
  }
  bool isNull() {return ptr == nullptr;};
};

class Test {
public:
  Test() {
    std::cout << "Test created" << std::endl;
  }
  ~Test() {
    std::cout << "Test destroyed" << std::endl;
  }
};

int main() {
  SmartPtr<Test> p1 {new Test()};
  std::cout << (p1.isNull() ? "p1 is null" : "p1 is not null") << std::endl;


  SmartPtr<Test> p2 {p1};
  std::cout << (p1.isNull() ? "p1 is null" : "p1 is not null") << std::endl;
  std::cout << (p2.isNull() ? "p2 is null" : "p2 is not null") << std::endl;
  return 0;
}

35. RValue References

  • Rvalue references are declared using a double ampersand &&. For example, int&& x {5}.
  • These references hold rvalues only and can not be initialized using lvalues.
  • Whenever using a literal to initialize an rvalue reference, a copy is contructed.
  • Unlike normal expressions, where the temporary objects are destroyed after the end of the expression; here, the object's lifespan is increased until the end of the block.

36. Move Constructors

  • Just like copy constructors, there are move constructors.
  • Copy constructors create a shallow/deep copy of the resource while move constructors simply transfer ownership of the provided resource.
  • Implementing move constructors is a good practice since it prevents copying in certain situations.
#include <iostream>

class Array {
private:
  int* m_arr {};
  int m_size {};

public:
  Array(int n) {
    m_arr = new int[n];
    m_size = n;
    for (int i = 0; i < n; i++) {
      m_arr[i] = i+1;
    }
  }
  // copy constructor (deep copy)
  Array(const Array& arr) {
    std::cout << "Copy constructor called" << std::endl;
    m_size = arr.m_size;
    m_arr = new int[m_size];
    for (int i {0}; i < m_size; i++) {
      m_arr[i] = arr.m_arr[i];
    }
  }

  // move constructor (notice the &&)
  Array(Array&& arr) {
    // transfering ownership
    std::cout << "Move constructor called" << std::endl;
    m_size = arr.m_size;
    m_arr = arr.m_arr;
    arr.m_arr = nullptr;
  }

  // copy assignment
  Array& operator=(const Array& arr) noexcept {
    std::cout << "Copy assignment called" << std::endl;
    if (&arr == this) {
      return *this;
    }

    delete m_arr;

    m_size = arr.m_size;
    m_arr = new int[m_size];
    for (int i = 0; i < m_size; i++) {
      m_arr[i] = arr.m_arr[i];
    }
    return *this;
  }

  // move assignment
  Array& operator=(Array&& arr) noexcept {
    std::cout << "Move assignment called" << std::endl;
    if (&arr == this) {
      return *this;
    }

    delete m_arr;

    m_size = arr.m_size;
    m_arr = arr.m_arr;
    arr.m_arr = nullptr;
    return *this;
  }

  friend std::ostream& operator<<(std::ostream& out, const Array& arr) {
    out << "[";
    if (arr.m_arr != nullptr) {
      for (int i = 0; i < arr.m_size; i++) {
        out << arr.m_arr[i];
        if (i < arr.m_size-1) out << ", ";
      }
    }
    out << "]";
    return out;
  }

};

Array test() {
  return Array(5);
}

int main() {
  Array x {5};
  Array y = test();
  std::cout << y << std::endl;
  return 0;
}

36.1. When is a move constructor called?

  • A move constructor is implicitly called when a function returns a local object by value.

Author: Prayag Jain

Created: 2025-01-25 Sat 00:17

Validate