cpp-notes
Table of Contents
- 1. Compiler
- 2. Basics
- 3. Functions and Files
- 4. Debugging
- 5. Data Types and Related Stuff
- 6. Constants and Strings
- 7. Operators
- 8. Bit Manipulation
- 9. Namespaces
- 10. Scope, Duration and Lifetime
- 11. Using Declarations
- 12. Control Flow
- 13. Randomness
- 14. Software Testing
- 15. Type Conversions, Type Aliases, Type Deduction
- 16. Function Overloading
- 17. Function Templates
- 18. References
- 19. Pointers
- 20. User Defined Types
- 21. Libraries
- 22. Object Oriented Programming
- 23. More on Classes
- 23.1. The
thiskeyword - 23.2. Returning
*this - 23.3. Resetting Class Objects
- 23.4. Classes and Header Files
- 23.5. Nested Types
- 23.6. Destructors
- 23.7. Class Templates with Member Functions
- 23.8. Static Members
- 23.9. Static Member Functions
- 23.10. Friend Functions
- 23.11. Friend Class
- 23.12. Ref Qualifiers
- 23.1. The
- 24. Vectors
- 25. Fixed Size Arrays
- 26. Iterators
- 27. Functions
- 28. Stack and Heap
- 29. Ellipses
- 30. Lambda Functions
- 31. Operator Overloading
- 31.1. Overloading using
friendfunctions - 31.2. Overloading using normal functions
- 31.3. Overloading I/O Operators
- 31.4. Overloading using member functions
- 31.5. Overloading unary operators
- 31.6. Overloading increment/decrement operators
- 31.7. Overloading the subscript operator
- 31.8. Overloading the parenthesis operator
- 31.9. Functors
- 31.10. Overloading typecasts
- 31.11. Overloading the assignment operator
- 31.12. Shallow and Deep Copy
- 31.1. Overloading using
- 32. Dynamic Memory Allocation
- 33. RAII
- 34. Smart Pointers
- 35. RValue References
- 36. Move Constructors
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.
- Declaration Statements
- Jump Statements
- Expression Statements
- Compound Statements
- Selection Statements (Conditionals)
- Iteration Statements
- Try Blocks
2.2. Variable Assignment
- 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. - Initialization
- Default Initialization No initial value is provided.
- Copy Initialization
Initializer is provided after equal sign.
Use of this is discouraged because it is considered inefficient.
int a = 5; - 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); 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
- Variable names should begin with lowercase characters.
- Function names should begin with lowercase characters.
- Identifier names starting with capital letters represent user defined types.
camelCaseorsnake_caseboth are fine but stay consistent.- You may mix them like
snake_casefor variable names andcamelCasefor function names. - 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
- Within a file, each function, variable, type or template can only have one definition (except variables in different local scopes) (violation causes compile error)
- With a program (multiple files), each variable can have only one definition (violation causes linker error)
- 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.
- Include Directive
Includes header files -
#include <iostream> - 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
- Conditional Directives
This includes the
ifdef,ifndefandendifdirectives.
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.his namedcstdlibin C++. - The bad way to include header files is to use relative paths in the
#includedirective. For example,#include "../myfile.h". - The better way is to specify the
include directoryto the compiler using the-Iflag-g++ main.cpp -I/src/includes -o main. There is no space after-Iin 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 oncein 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
- Comment out code
- Use
std::cerrinstead ofstd::coutbecausestd::cerris unbuffered so output is instant. - 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_tandint_least8_tbehave likecharinstead of integer values so prefer using the 16bit versions. So, if you try to print a variable of typeint_fast8_twith 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
sizeofoperator is alsostd::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_tcan hold is considered ill-formed.
5.3. Floating Point Precision
- By default,
std::coutdisplays only upto 6 significant digits of floating point numbers. You can change the precision by usingstd::cout << std::setprecision(<any number>). - Prefer
doubleoverfloatfor 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
stdboollike in C to usetrueandfalse. - Sending
trueandfalsevalues 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>) variableto cast to different types, in C++, we usestatic_castoperator.#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_castoperator 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-
0xfor hexadecimal, eg-int a{0x1F}0for octal, eg-int a{012}0bfor 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
constexprkeyword to tell the compiler that it's a compile time constant (usingconstdoesn'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
inlinekeyword 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,
constexpris used to tell the compiler that a variable is a compile-time constant. constexprcan 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
constexprfunction 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
constevalkeyword. - 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.
constevalandconstexprfunctions are implicitlyinlinebecause 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::wsis an input manipulator which allows getline to ignore any leading whitespace characters already present in the input buffer.std::wsis not preserved across calls so it must be used with every getline call.- Use
str.length()orstd::ssize(str)(defined in<string>) to get the length of a string namedstr. - Initializing using
std::stringis 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::swill form anstd::stringliteral, E.g."hello"std::string_literals::s.
6.6. std::string_view
- Whenever an
std:stringis initialized, the string literal provided is copied into the string object (expensive operation). - Even in functions, when an
std::stringis passed by value, it is copied into the function parameter (expensive operation). std::string_viewis a lightweight alternative tostd::string.- Always prefer
std::string_viewoverstd::stringunless you need to modify the string. - It lives under the
<string_view>header. - Assigning a new string to a
std::string_viewwill not change the original string, it will just point to the new string. - You can use the
std::string_view_literals::svsuffix to form astd::string_viewliteral. - Unlike
std::string,std::string_viewcan be used inconstexprcontexts. - The
remove_prefix(count)andremove_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,ymeans 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
>>(right shift) (eg- 0011 >> 1 = 0001, 0011 >> 2 = 0000)<<(left shift) (eg- 0011 << 1 = 0110, 0011 << 2 = 1100)&(bitwise and) (eg- 0101 & 0011 = 0001)|(bitwise or) (eg- 0101 | 0011 = 0111) (only one bit should be 1 to be evaluated as true)^(bitwise xor) (eg- 0101 ^ 0011 = 0110)~(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::bitsetinstead ofstd::uint8_ttoo!
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
-Wshadowflag 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-
- External Linkage
- Internal Linkage
- 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
statickeyword. - Functions have external linkage by default but can also be made to have internal linkage using the
statickeyword.
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
externkeyword. - Global variables and functions have external linkage by default so you don't need to use the
externkeyword.
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
constexprcan be made to have external linkage by using theexternkeyword 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
constexprand then include the header file in all files where you want to use the constants. - This approach has some downsides-
- 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.
- If the constants are large, it will increase memory usage because of duplication!
- Another approach is to define a namespace in a
.cppfile, define the constants usingextern constin the namespace, forward declare the constants in a.hheader file and then include the header file in all files where you want to use the constants. - This approach also has some downsides-
- These constants will now be considered compile-time only in the
.cppfile in which they are defined because the linker will only see the forward declaration from the header file.
- These constants will now be considered compile-time only in the
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
breakorreturnkeyword, 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 anullstatement.
12.4. Halting Programs
- You can halt a program using
std::exit()fromcstdlib. - 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 wheneverstd::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
seedis 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).
mt19937generates 32 bit unsigned integers whilemt19937_64generates 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_distributionclass.
#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::chronoto 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_devicealso 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_seqcan be used to provide a seed sequence to the PRNG.- We can provide as many random values to
std::seed_seqas 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
assertto 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
charwhenintwas 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
assertmacro 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
NDEBUGflag 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.
- Floating Point Promotions
A value of type float is promoted to double.
- 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.
- Safe Conversions
Conversions in which the original value (the meaning) does not change. For example, converting an int to a long.
- Reinterpretative Conversions
Conversions which are considered unsafe where the result may be outside the range of the source type. This includes signed/unsigned conversions.
- 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_castoperator.
15.3. Type Aliases
- We use the
usingkeyword 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
typedeffrom C in C++ (but it's only present for backwards compatibility and its syntax can get very confusing). - The
std::int8_ttype is just an alias tocharwhich 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 literal5.76has the type double and we have explicitly mentioneddoubleas the data type ofdistance, 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
autowill dropconstandconstexprfrom 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 usestd::stringorstd::string_view, use literal suffixes-"hello"std::literals::sor"hello"std::literals::sv. - In such cases, it may be better to not use
autoand just use the type explicitly. You can also use
autowith 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
constare 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-
- The compiler will look for an exact match for the function call.
- If step 1 fails, the compiler will perform numeric promotions and look for a match.
- If step 2 fails, the compiler will perform numeric conversions and look for a match.
- If step 3 fails, the compiler will look for a match using the user-defined conversions.
- If step 4 fails, the compiler will look for a matching function that uses ellipsis.
- If step 5 fails, the compiler will throw an error.
16.3. Deleting Functions
- You can prohibit certain function calls by using the
deletekeyword.
#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 thisint 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
typenameor theclasskeyword is used to specify a template parameter (it doesn't matter if you useclassortypename, 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 foradddoes 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 simplyadd(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
constexprvalue. - The following types are accepted as template parameters-
- Integral types
- Enumeration type
- Floating Point type (since C++20)
- Literal class types (since C++20)
- Etc…
- From C++17 onwards, you can use the
autokeyword 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, wherexis a variable (something like&5won'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
&operatorint // 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
autokeyword 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
autowill 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
constkeyword.
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_viewis better thanstd::string&because when passing different types of strings (c-style string literals, stringview, string), thestd::string_viewwill be able to reference all those types easily, while passing types other thanstd::stringto astd::string&will require the compiler to make a temporarystd::stringobject (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
nullptrliteral to initialize a null pointer explicitly-int* ptr {nullptr}; nullptrhas the typestd::nullptr_t. So whenever nullptr is used to initialize a pointer variable, it is implicitly converted fromstd::nullptr_tto 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
nullptrto 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
- Enumerated Types
- Class Types
- Structs
- Classes
- 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
enumkeyword.
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; }
- 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-
- They don't implicitly convert to integers
- 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'sstd::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
- 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.
- 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; }
- 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.
- 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
.likething.first. - When passing by pointers, you have to access properties by using
->likething->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 {}; }
- 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; }
- 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; }
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
firstandsecondas 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.
- 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
.liband 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
.dlland 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
constkeyword. - 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
constas a good practice.
- You can overload the same member function with the exact same definition by adding a variant without the
constkeyword and one with theconstkeyword.
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!
- 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
- 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_likem_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!
- 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_viewfor anstd::stringbecause 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).
- Naming Constructors
- Constructors have special rules which must be followed while naming them.
- The function must have the same name as that of the class type (with the same case).
- 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; }
- Constructors have special rules which must be followed while naming them.
- 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; }
- 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() {}; };
- 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} {}; };
- 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; }
- 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}); }
- 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
= deletesyntax.
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; };
- 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.
- 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
explicitkeyword 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
*thiskeyword 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::Typeand its enumerators can be accessed like this-Fruit:apple. - We've used
enuminstead ofenum classbecause classes have their own scope so we don't need to worry about polluting the global scope. - You can also use
typedefand 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-
- It should have the same name as that of the class but prefixed with a tilde
~. - It cannot take any arguments.
- It cannot return anything.
- It should have the same name as that of the class but prefixed with a tilde
#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
Tmight be expensive to copy, it's recommended to useconst 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
.cppfiles, 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:
- It is a
constvariable- It is a
constexprvariable- It is an
inlinevariable
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
*thispointer 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
*thispointer.
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-
- If the function returns by value (rvalue), it can be used safely anywhere but might be inefficient to copy.
- 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-
- Most developers don't know about this so this can cause errors or inefficiencies in use.
- Use of this can be prevented easily by using good practices.
24. Vectors
std::vectoris 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::vectorhas 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. constvectors 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 usestd::arrayfor 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::sizeto get the size of any type of array:std::size(myArr). - C++20 introduced a non-member
std::ssizefunction 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 intvariables to access elements without getting narrowing conversion warnings (becausestd::size_tis required). - We should use
std::size_tfor iterations and accessing elements but this is an unsigned type and might not always be desirable.. - If we want, we can use
intorstd::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::stringandstd::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
autokeyword 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
push_back(): push elementspop_back(): pop elementsback(): peek the top elementemplace_back(): push elements (more efficient when pushing objects)
- The
push_backandpop_backfunctions will increment the length of the vector, and reallocate memory if necessary. - The
resizemethod 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 usepush_back()when you need to push anything else to a vector.
25. Fixed Size Arrays
- The primary reason why we should not use
std::vectoreverywhere is that they can't be madeconstexpr. constexprarrays 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,
constexprvariable or an unscoped enum. std::arrayis 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
constexpressions asconstexprsoconstalso works!
If you're using an
std::arraywithout marking it asconstexprconsider using a vector instead lol
- The second template argument is a non-type template argument of type
std::size_twhich must beconstexpr. - 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::getnon-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_wrapperis 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::refandstd::crefas aliases tostd::reference_wrapperandconst std::reference_wrapperrespectively.
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()andstd::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:
- Code Segment: Where the compiled program sits in memory.
- BSS Segment: Where zero-initialized global and static variables are stored.
- Data Segment: Where initialized global and static variables are stored.
- Heap: Where dynamically allocated variables are allocated from.
- 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
constand can not be modified. - We can mark the lambda
mutableto 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 + yis equivalent tooperator+(x, y). - Almost all operators can be overloaded in C++ to work with different data types except:
- Conditional:
?: sizeof- Scope:
:: - Member selection:
. - Pointer member selector:
.* typeid- Casting operators
- Conditional:
- 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
- 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 isstd::ostreamwhich 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:
- If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function.
- If you’re overloading a unary operator, do so as a member function.
- 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.
- 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.
- 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 anintparameter 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_castoperator.
31.11. Overloading the assignment operator
- This must be implemented as a member function only.
- We can return
*thispointer 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
newkeyword just likemalloc()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
newto returnnullptrinstead of throwing an error-int* n {new (std::nothrow) int{}};
32.2. Memory Leaks
- Memory leaks are caused in two situations:
- When a program does not free the memory of a pointer after it terminates.
- 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
voidpointer. - 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.