- Modern C++
- Modern C++: Build and Tools (Lecture 1, I. Vizzo, 2020)
- Modern C++: Core C++ (Lecture 2, I. Vizzo, 2020)
- Modern C++: C++ Functions (Lecture 3, I. Vizzo, 2020)
- Modern C++: The C++ STL Library (Lecture 4, I. Vizzo, 2020)
- STL Algorithms
- Modern C++: I/O Files (Lecture 5, I. Vizzo, 2020)
- Modern C++ Classes
- Example class definition
- What about structs?
- Always initialize structs using braced initialization
- Constructors and Destructor
- Many ways to create instances
- Setting and getting data
- Declaration and definition
- Always initialize members for classes
- Const correctness
- Move semantics
- Custom operators for a class
- Class Special Functions
- Static variables and methods
using
for type aliasing- Enumeration classes
- Modern C++: Object Oriented Design (Lecture 7, I. Vizzo, 2020)
- Modern C++: Memory Management (Lecture 8, I. Vizzo, 2020)
- Using pointers for classes
- Pointers are polymorphic
- this pointer
- Using const with pointers
- Stack memory
- Heap memory
- Memory leak
- Dangling pointer
- RAII
- Shallow vs deep copy
- Smart pointers
- C++11 smart pointers types
- Smart pointers manage memory!
- std::unique_ptr
- Shared pointer (std::shared_ptr)
- When to use what?
- Typical beginner error
- Modern C++ Course: Templates (Lecture 9, I. Vizzo, 2020)
- This lecture
- Resources
- STL algorithm
- C++ Utilities
- Google C++ Testing
- CPP-06 Modern C++: Static, Numbers, Arrays, Non-owning pointers, Classes (2018, Igor)
- CppCon 2016: Arthur O'Dwyer “Template Normal Programming (part 1 of 2)”
-std=c++17
,-o
, etc- Enable all warnings, treat them as errors
-Wall, -Wextra, -Werror
- Optimization options
-O0
-- no optimizations [default]-O3
or-Ofast
-- full optimizations
- Keep debugging symbols
-g
-
#pragma once
- In the C and C++ programming languages,
#pragma once
is a non-standard but widely supported preprocessor directive designed to cause the current source file to be included only once in a single compilation.#progma once struct foo { int member; };
#pragma once
serves the same purpose as include guards, but with several advantages
- In the C and C++ programming languages,
-
include guards
#ifndef GRANDPARENT_H #define GRANDPARENT_H struct foo { int member; }; #endif /* GRANDPARENT_H */
-
Create a static library with
ar rcs libname.a module.o module.o ...
-
Static libraries are just archives just like zip/tar/...
-
Linking: to use a library we need
- A header file
library_api.h
- The compiled library object
libmylibrary.a
- A header file
-
Use modules and libraries
- compile modules,
-c
to assemble and generate object filec++ -std=c++17 -c tools.cpp -o tools.o
- organize modules into libraries
ar rcs libtools.a tools.o <other_modules.o ...>
- link libraries when building code
c++ -std=c++17 main.cpp -L . -ltools -o main
-L <dir>
tell compile to search libraries in this path-l
specify the library you want to link
- compile modules,
- They began as
shell
scripts - They turn into
MakeFiles
- And now into MetaBuild Systems like
CMake
- Accept it, CMake is not a build system
- It's a build system generator
- You need to use an actual build system like
Make
ofNinja
-
Build process from the user's perspective
cd <project_folder>
mkdir build
cd build
cmake ..
make
-
The build process is completely defined in
CMakeLists.txt
-
And childrens src/CMakeLists.txt, etc
First CMakeLists.txt
# first CMakeLists.txt cmake_minimum_required(VERSION 3.1) # Mandatory. project(first_project) # Mandatory. set(CMAKE_CXX_STANDARD 17) # Optional. # export compile commands in a json file set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Optional. important # tell cmake where to look for *.hpp, *.h files include_directories(include/) # create library "libtools" add_library(tools src/tools.cpp) # creates libtools.a # create executable "main" add_executable(main src/main.cpp) # creates main # tell the linker to link "main" with "libtools" target_link_libraries(main tools)
-
More CMake directive
- set cxx flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror") # Optional.
- set debug & optimization
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
- set build type if not set
if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release") endif()
- set cxx flags
-
User pre-compiled library
- For example, given
libtools.so
it can be used in the project as follows# find library named tools in find_library(TOOLS NAMES tools PATHS ${LIBRARY_OUTPUT_PATH} ) # use it for linking target_link_libraries(first_project ${TOOLS})
- For example, given
-
Search for some_pkg.cmake file and load it
# search for some_pkg.cmake file and load it find_package(some_pkg REQUIRED)
- Iterating over a standard containers like array or vector has simple syntax
- Avoid mistakes with indices
- added in c++11
for (const auto& value: container) { // this happens for each value in the container }
-
New in c++17
#include <map> std::map<char, int> my_dict{{'a', 1}, {'b', 2}}; for (const auto &[key, value] : my_dict) { std::cout << key << " " << value << std::endl; }
-
Similar to
for key, value in my_dict.items(): print(key, value)
-
implemented as a tree, so it does not have a
reserve
method to set capacity.
- We can create a reference to any variable
float& ref = original_variable; std::string& hello_ref = hello;
- Reference is part of type: variable
ref
has typefloat&
- Whatever happens to a reference happens to the variable and vice versa
- Yields performance gain as references avoid copying data.
std::stringstream filename("205.txt");
int num = 0;
std::string ext;
// split the string stream using simple syntax
filename >> num >> ext;
std::cout << "num: " << num << std::endl; // 205
std::cout << "ext: " << ext << std::endl; // .ext
#include <string>
const std::string source("copy this!"); std::string dest = source;
auto GetDictionary() {
return std::map<char, int> my_dict{{'a', 1}, {'b', 2}};
}
- with the introduction of structured binding in c++17 you can now:
#include <tuple> auto Foo() { return make_tuple("Super Variable", 5); } int main() { auto [name, value] = Foo(); ... }
- But in fact, in C++11, this is the preferred way:
std::vector<X> f();
- With C++11, std::vector has move-semantics, which means the local vector declared in your function will be moved on return and in some cases even the move can be elided by the compiler.
- RVO: Return value optimization
void f(type arg1, type arg2) {
// f holds a copy of arg1 and arg2 }
}
void f(type& arg1, type& arg2) {
// f holds a reference of arg1 and arg2 }
// f could possibly change the content of arg1 or arg2
}
void f(const type& arg1, const type& arg2) {
// f can't change the content of arg1 or arg2
}
- Only set in declaration, not in definition
- Cons:
- Evaluated upon every call
- Values are hidden in declaration
- Can lead to unexpected behavior when overused
- Only use them when readability gets much better.
- Pass by reference to avoid copy
- Avoid using non-const ref
- use const refs to ensure the object passed in won't be modified unexpectedly
- non-const refs are mostly used in older code written before c++11
-
helps avoiding name conflicts
-
group the project into logical moduels
namespace module_1 { int SomeFunc() {} }
namespace module_2 { int SomeFunc() {} }
-
Avoid using namespace
<name>
// Avoid !!! using namespace std;
-
Only use what you need
using std::cout; // explicitly use cout. using std::endl;
-
Never use
using namespace <name>
in *.hpp files- Prefer using explicit
using
even in *.cpp files
- Prefer using explicit
- If you find yourself relying on some constants in a file and these constants should not be seen in any other file, put them into a nameless namespace on the top of this file
namespace { const int kLocalImportantInt = 13; const int kLocakImportantFloat = 13.0f; } // nameless ...
#include <array>
to use std::array- Store a collection of items of same type
- Create from data:
array<float, 3> arr = {1.0f, 2.0f, 3.0f};
- Access items with
arr[i]
- Number of stored items:
arr.size()
- Useful access aliases:
- First item:
arr.front() == arr[0]
- Last item:
arr.back() == arr[arr.size() - 1]
- First item:
std::array
#include <array>
#include <iostream>
int main() {
std::array<float, 3> data{10.0F, 100.0F, 1000.0F};
for (const auto &elem : data) {
std::cout << elem << std::endl;
}
// std::cout << std::boolalpha;
std::cout << "Array empty: " << data.empty() << std::endl;
std::cout << "Array size : " << data.size() << std::endl;
}
#include <vector>
to use std::vector- Vector is implemented as a dynamic table
- Remove all elements:
vec.clear()
- Add a new item in one of two ways:
vec.emplace_back(value)
[preferred, c++11]vec.push_back(value)
[historically better known]
- Use it! It is fast and flexible!
- Consider it to be a default container to store collections of items of any same type.
std::vector
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3};
std::vector<std::string> names = {"Nacho", "Cyrill"};
names.emplace_back("Roberto");
std::cout << "First name : " << names.front() << std::endl;
std::cout << "Last number: " << numbers.back() << std::endl;
return 0;
}
reserve(n)
ensures that the vector has enough capacity to store n items- This is a very important optimization
vector <int> vec; // size 0, capacity 0 vec.reserve(N); // size 0, capacity 100
#include <map>
- sorted associative container
- Contains key-value pairs
- keys are stored using the
<
operator- your keys should be comparable
- create from data
std::map<KeyT, ValueT> m{{key1, value1}, {...} };
- check size:
m.size()
- Add item to map:
m.emplace(key, value);
- modify or add item:
m[key] = value;
- Get const ref to an item: m.at(key);
- check if key present:
m.count(key)> 0
m.contains(key) [bool]
starting in c++20
std::map
#include <iostream> // I/O stream for simple input/output
#include <map>
int main() {
using StudentList = std::map<int, std::string>;
StudentList students;
students.emplace(1509, "Nacho");
students[1508] = "Paco";
for (const auto &[id, name] : students) {
std::cout << id << " " << name << std::endl;
}
}
#include <unordered_map>
- implemented as a hash table
- key type has to be hashable
- faster to use than std::map
- in contrast to
map
,std::unordered_map
has the concept of capacity, it hasreserve
method.- add
clear
won't affect its capacity.
- add
for (const auto &student : students) {
std::cout << student.first << " " << student.second << std::endl;
}
New in C++17
for (const auto &[id, name] : students) {
std::cout << id << " " << name << std::endl;
}
container | desc |
---|---|
array (c++11) | static contiguous array |
vector | dynamic contiguous array |
deque | double-ended queue |
forward_list (c++11) | single-linked list |
list | doubly-linked list |
Associative containers implement sorted data structures that can be quickly searched (O(logn) complexity).
container | desc |
---|---|
set | sorted by keys |
map | sorted by keys |
multiset | keys may be non-unique, sorted by keys |
multimap | keys may be non-unique, sorted by keys |
container | desc |
---|---|
unordered_set | hashed by keys |
unordered_map | hashed by keys |
unordered_multiset | keys may be non-unique, hashed by keys |
unordered_multimap | keys may be non-unique, hashed by keys |
To be able to use std::unordered_map (or one of the other unordered associative containers) with a user-defined key-type, you need to define two things:
- A hash function; this must be a class that overrides
operator()
and calculates the hash value given an object of the key-type.- One particularly straight-forward way of doing this is to specialize the
std::hash
template for your key-type.
- One particularly straight-forward way of doing this is to specialize the
- A comparison function for equality;
- this is required because the hash cannot rely on the fact that the hash function will always provide a unique hash value for every distinct key (i.e., it needs to be able to deal with collisions), so it needs a way to compare two given keys for an exact match.
- You can implement this either as a class that overrides operator(), or as a specialization of std::equal, or – easiest of all – by overloading operator==() for your key type (as you did already).
Example
For example, assuming a Key
type like this:
struct Key
{
std::string first;
std::string second;
int third;
bool operator==(const Key &other) const
{ return (first == other.first
&& second == other.second
&& third == other.third);
}
};
namespace std {
template <>
struct hash<Key>
{
std::size_t operator()(const Key& k) const
{
using std::size_t;
using std::hash;
using std::string;
// Compute individual hash values for first,
// second and third and combine them using XOR
// and bit shifting:
return ((hash<string>()(k.first)
^ (hash<string>()(k.second) << 1)) >> 1)
^ (hash<int>()(k.third) << 1);
}
};
}
Container adaptors provide a different interface for sequential containers
adaptors | desc |
---|---|
stack | adapts a container to provide LIFO data structure |
queue | adapts a container to provide FIFO data structure |
priority_queue | adapts a container to provide priority queue |
Iterators are the glue that ties standard-library algorithms to their data.
Iterators are the mechanism used to minimize an algorithm's dependence on the data structures on which it operates.
sort() -----\ /----- vector
find() -----\ /----- map
merge() ------ Iterators ------ list
... ------ ------ ...
my_algo() -----/ \----- my_container
your_fct() -----/ \----- your_container
STL uses iterators to access data in containers
- Iterators are similar to pointers
- Allow quick navigation through containers
- Most algorithms in STL use iterators
- Defined for all using STL containers
- Access current element with
*iter
- Acceptes
->
alike to pointers - Move to next element in container
iter++
- Prefer range-based for loops
- Compare iterators with
==, !=, <
the leading c
means const
begin, cbegin
- returns an iterator to the beginning of a container for array
end, cend
- returns an iterator to the end of a container for array
rbegin, crbegin
- return a reverse iterator to a container to array
rend, crend
- return a reverse end iterator for a container or array
iter example
#include <iostream> // I/O stream for simple input/output
#include <map>
#include <vector>
int main() {
std::vector<double> x{1, 2, 3};
for (auto it = x.begin(); it != x.end(); ++it) {
std::cout << *it << std::endl;
}
std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
std::map<int, std::string>::iterator it = m.find(1);
std::cout << it->first << ":" << it->second << std::endl;
auto it2 = m.find(1); // same thing
std::cout << it2->first << ":" << it2->second << std::endl;
if (m.find(3) == m.end()) {
std::cout << "3 not found" << std::endl;
}
return 0;
}
- About 80 standard algorithms
- Defined in
#include <algorithm>
- They operate on sequences defined by a pair of iterators(for inputs ) or a single iterator (for outputs).
- Don't reinvent the wheel
- before writting you own
sort
function ... - https://en.cppreference.com/w/cpp/algorithm
- before writting you own
- When using STL containers,
std::vector, std::array
, etc. Try to avoid writing your own algorithms - If you are not using STL containers, then providing implementations for the stardard iterators will give you access to all algorithms for free.
std::array<int, 10> s = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
std::sort(s.begin(), s.end());
std::array<int, 10> s = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
auto result1 = std::find(s.begin(), s.end(), 7);
if (result1 != s.end()) {
std::cout << "Found 7 at index " << std::distance(s.begin(), result1)
<< std::endl;
} else {
std::cout << "7 not found" << std::endl;
}
std::fill(v.begin (), v.end (), -1);
how many time a value appears in the container
int num_items1 = std::count(v.begin (), v.end (), n1);
inline bool div_by_3(int i) { return i % 3 == 0; }
int main () {
std::vector <int> v{1, 2, 3, 3, 4, 3, 7, 8, 9, 10};
int n3 = std::count_if(v.begin (), v.end (), div_by_3);
...
}
std::vector <int> nums {3, 4, 2, 8, 15, 267};
// lambda expression
auto print = [](const int& n) { cout << " " << n; };
std::for_each(nums.cbegin (), nums.cend (), print);
inline bool even(int i) { return i % 2 == 0; };
int main () {
std::vector <int> v(10, 2); // 2 2 2 2 2 2 2 2 2 2
std::partial_sum (v.cbegin (), v.cend (), v.begin ());
// 2 4 6 8 10 12 14 16 18 20
bool all_even = all_of(v.cbegin (), v.cend (), even);
// true
std::vector <int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::rotate(v.begin (), v.begin () + 2, v.end ());
// before rotate: 1 2 3 4 5 6 7 8 9 10
// after rotate: 3 4 5 6 7 8 9 10 1 2
auto UpperCase (char c) { return std::toupper(c); }
int main () {
const std::string s("hello");
std::string S{s};
std::transform (s.begin (), s.end (), S.begin (), UpperCase );
// s: hello
// S: HELLO
}
std::vector <int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sum = std::accumulate (v.begin (), v.end (), 0); // 55
int product = std::accumulate (v.begin (), v.end (), 1, std::multiplies ()); // 3628800
using std::max;
cout << "max(1, 9999) : " << max(1, 9999) << endl;
cout << "max('a', 'b'): " << max('a', 'b') << endl;
int main() {
std::vector<int> v{3, 1, 4, 1, 0, 5, 9};
auto result = std::min_element(v.begin(), v.end());
std::cout << "The minimum element is " << *result << std::endl; // 0
auto min_location = std::distance(v.begin(), result);
std::cout << "min at: " << min_location << std::endl; // min at: 4
return 0;
}
using std::minmax_element ;
auto v = {3, 9, 1, 4, 2, 5, 9};
auto [min , max] = minmax_element (begin(v), end(v));
cout << "min = " << *min << endl;
cout << "max = " << *max << endl;
// min = 1
// max = 9
// value should be between [kMin ,kMax]
const double kMax = 1.0F;
const double kMin = 0.0F
cout << std::clamp (0.5 , kMin , kMax) << endl; // 0.5
cout << std::clamp (1.1 , kMin , kMax) << endl; // 1
cout << std::clamp (0.1 , kMin , kMax) << endl; // 0.1
cout << std::clamp (-2.1, kMin , kMax) << endl; // 0
#include <algorithm>
#include <iostream> // I/O stream for simple input/output
#include <random>
#include <vector>
int main() {
std::string in = "C++ is cool", out;
auto rnd_dev = std::mt19937{std::random_device{}()};
const int kNLetters = 5;
std::sample(in.begin(), in.end(), std::back_inserter(out), kNLetters,
rnd_dev);
std::cout << "from : " << in << std::endl; // C++ is cool
std::cout << "sample: " << out << std::endl; // sample: + ol // random
return 0;
}
- two groups libraries
- language support libraries
- type support (
std::size_t
) - dynamic memory management (
std::shared_ptr
) - error handling (
std::exception, assert
) - initializer list(
std::vector{1,2}
) - much more...
- type support (
- general-purpose libraries
- program utilities (
std::abort
) - date and time (
std::chrono::duration
) - optional, variant and any (
std::variant
) - pairs and tuples (
std::tuple
) - swap, forward and move (
std::move
) - hash support (
std::hash
) - formatting library (coming in c++20)
- much more...
- program utilities (
- language support libraries
int a = 3;
int b = 5;
std::swap(a, b); // a:5, b:3
- safty union
- 如何优雅的使用 std::variant 与 std::optional
// can be either int or float, but not both
std::variant <int, float> v1;
v1 = 12; // v contains int
cout << std::get <int>(v1) << endl; // 12
// will raise error if you want to get float
v1 = 1.0; // override value
// get the index of stored type
std::cout << "v1 - " << v1.index() << std::endl;
std::variant <int, float> v2 {3.14F};
cout << std::get <1>(v2) << endl; // 3.14
v2 = std::get <int>(v1); // assigns v1 to v2
v2 = std::get <0>(v1); // same assignment
v2 = v1; // same assignment
cout << std::get <int>(v2) << endl; // 12
- when to use ?
- a function may return different types of return values.
- For example: the formula for finding the root of a quadratic equation in one variable,
- the root may be one, or two, or none
using Two = std::pair<double, double>; using Roots = std::variant<Two, double, void*>; Roots FindRoots(double a, double b, double c) { auto d = b*b-4*a*c; if (d> 0.0) { auto p = sqrt(d); return std::make_pair((-b + p) / 2 * a, (-b - p) / 2 * a); } else if (d == 0.0) return (-1*b)/(2*a); return nullptr; } struct RootPrinterVisitor { void operator()(const Two& roots) const { std::cout << "2 roots: " << roots.first << " " << roots.second << '\n'; } void operator()(double root) const { std::cout << "1 root: " << root << '\n'; } void operator()(void *) const { std::cout << "No real roots found.\n"; } }; TEST_F(TestFindRoot) { std::visit(RootPrinterVisitor(), FindRoots(1, -2, 1)); //(x-1)*(x-1)=0 std::visit(RootPrinterVisitor(), FindRoots(1, -3, 2)); //(x-2)*(x-1)=0 std::visit(RootPrinterVisitor(), FindRoots(1, 0, 2)); //x*x - 2 = 0 }
- polymorphism
using Draw = std::variant<Triangle, Circle>; Draw draw; std::vector<Draw> draw_list {Triangle{}, Circle{}, Triangle{}}; auto DrawVisitor = [](const auto &t) { t.Draw(); }; for (const auto &item : draw_list) { std::visit(DrawVisitor, item); }
- a function may return different types of return values.
- The time complexity of std::visit to obtain the type actually stored in std::variant is O(1), and the performance will not decrease with the increase of types in std::variant.
std::any a; // any type
a = 1; // int
std::cout << std::any_cast<int>(a) << std::endl;
a = 3.14; // double
std::cout << std::any_cast <double>(a) << std::endl; // 3.14
a = true; // bool
std::cout << std::boolalpha << std::any_cast <bool>(a) << std::endl; // true
- this is usually something we can solve using like
if/else
#include <iostream> // I/O stream for simple input/output
#include <optional>
std::optional<std::string> StringFactory(bool create) {
if (create) {
return "C++17";
}
return {}; // return nothing
}
int main() {
std::cout << StringFactory(true).value() << '\n'; // C++17
std::cout << StringFactory(false).value_or(":(") << '\n'; // :(
return 0;
}
std::tuple <double , char , string> student1;
using Student = std::tuple <double , char , string>;
Student student2 {1.4 , 'A', "Jose"};
cout << std::get <string>( student2) << endl; // Jose
cout << std::get <2>( student2) << endl; // Jose
// C++17 structured binding:
auto [gpa , grade , name] = make_tuple (4.4 , 'B', "");
- there are much better ways of doing benchmarking but let's say that you want to benchmark a function...
auto start = std::chrono::steady_clock::now ();
cout << "f(42) = " << fibonacci (42) << '\n'; // f(42) = 267914296
auto end = std::chrono::steady_clock::now ();
std::chrono::duration <double> sec = end - start;
cout << "elapsed time: " << sec.count () << "s\n"; // elapsed time: 1.84088s
- Only used for exceptional behavior
- Often misused : e.g. wrong parameter should not lead to an exception
- GOOGLE-STYLE Don’t use exceptions
- We can throw an exception if there is an error
- STL defines classes that represent exceptions. Base
class:std::exception
#include <stdexcept>
- An exception can be caught at any point of the program (
try - catch
) and even thrown further (throw) - The constructor of an exception receives a string error message as a parameter
- This string can be called through a member function
what()
- This string can be called through a member function
- Runtime Error:
string msg = "specific error string"; throw runtime_error (msg);
- Logic Error: an error in logic of the user
throw logic_error (msg);
- they are due to errors in the internal logic of the program. In theory, they are preventable.
try {
x = someUnsafeFunction (a, b, c);
}
// we can catch multiple types of exceptions
catch ( runtime_error &ex ) {
cerr << "Runtime error: " << ex.what () << endl;
} catch ( logic_error &ex ) {
cerr << "Logic error: " << ex.what () << endl;
} catch ( exception &ex ) {
cerr << "Some exception: " << ex.what () << endl;
} catch ( ... ) { // all others
cerr << "Error: unknown exception" << endl;
}
- Use streams from STL
- Syntax similar to
std::cerr
,ctd::cout
#include <fstream>
using Mode = std :: ios_base :: openmode;
// ifstream: stream for input file
std :: ifstream f_in(string& file_name , Mode mode);
// ofstream: stream for output file
std :: ofstream f_out(string& file_name , Mode mode);
// stream for input and output file
std :: fstream f_in_out(string& file_name , Mode mode);
- modes under which a file can be opened
Mode Meaning ios_base::app append output ios_base::ate seek to EOF when opened ios_base::binary open file in binary mode ios_base::in open file for reading ios_base::out open file for writing ios_base::trunc overwrite the existing file
- Use it when:
- The file contains organized data
- Every line has to have all columns
1 2.34 One 0.21 2 2.004 two 0.23 3 -2.34 string 0.22
#include <fstream>
#include <iostream>
#include <string>
int main() {
int i;
double a, b;
std::string s;
std::ifstream in("test.txt"); // default mode: std::ios_base::in
// Read data, until it is there.
// even if there are empty lines and leading spaces
while (in >> i >> a >> s >> b) {
std::cout << i << " " << a << " " << s << " " << b << std::endl;
}
return 0;
}
// 1 2.34 One 0.21
// 2 2.004 two 0.23
// 3 -2.34 string 0.22
#include <fstream>
#include <iostream>
int main() {
std::string line, file_name;
std::ifstream input("test_bel.txt");
// Read data line-wise
while (std::getline(input, line)) {
// std::cout << line << std::endl;
// Strting has a find method
std::string::size_type loc = line.find("filename");
if (loc != std::string::npos) {
// Get the file name
file_name = line.substr(line.find("=", 0) + 1, std::string::npos);
std::cout << "File name: " << file_name << std::endl;
// File name: /home/ivizzo /. bashrc
}
}
return 0;
}
// line in file: `filename = /home/ivizzo /. bashrc`
#include <fstream>
#include <iomanip> // for setprecision
int main() {
std::string filename = "out.txt";
std::ofstream out(filename);
if (!out.is_open()) {
return EXIT_FAILURE;
}
double a = 1.23456789;
out << "Just string" << std::endl;
out << std::setprecision(10) << a << std::endl;
return 0;
}
// out.txt
// Just string
// 1.2345678899999998901
- fast
- No precision loss for floating point types
- syntax:
file.write(reinterpret_cast <char*>(&a), sizeof(a));
00000000: 0200 0000 0300 0000 0000 0000 0000 0000
00000010: 0000 0000 0000 0000 0000 0000 0000 0000
00000020: 0a
- syntax
file.read(reinterpret_cast <char*>(&a), sizeof(a));
#include <fstream>
#include <iostream>
#include <vector>
int main() {
std::string filename = "image.dat";
int r = 0, c = 0;
std::ifstream in(filename, std::ios::binary);
if (!in) {
return EXIT_FAILURE;
}
in.read(reinterpret_cast<char *>(&r), sizeof(r));
in.read(reinterpret_cast<char *>(&c), sizeof(c));
std::cout << "r = " << r << ", c = " << c << std::endl;
std::vector<float> data(r * c, 0);
in.read(reinterpret_cast<char *>(data.data()), data.size() * sizeof(float));
for (float d : data) {
std::cout << d << " ";
}
std::cout << std::endl;
return 0;
}
// r = 2, c = 3
// 0 0 0 0 0 0
- Use to perform operations on:
- paths
- regular files
- directories
- like a utility library on top of the IO library
- Inspired in
boost::filesystem
- Makes your life easier.
- https://en.cppreference.com/w/cpp/filesystem
#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
int main() {
// PS. it's create_directories, NOT create_directory
fs::create_directories("sandbox/a/b"); // mkdir -p
std::ofstream("sandbox/file1.txt");
std::ofstream("sandbox/file2.txt");
// walk
for (auto &p : fs::recursive_directory_iterator("sandbox")) {
std::cout << p.path() << std::endl;
}
fs::remove_all("sandbox"); // rm -rf sandbox
return 0;
}
// "sandbox/file2.txt"
// "sandbox/file1.txt"
// "sandbox/a"
// "sandbox/a/b"
#include <filesystem>
#include <iostream>
namespace fs = std ::filesystem;
int main() {
std::cout << fs::path("/foo/bar.txt").filename() << '\n' // "bar.txt"
<< fs::path("/foo/.bar").filename() << '\n' // ".bar"
<< fs::path("/foo/bar/").filename() << '\n' // ""
<< fs::path("/foo/.").filename() << '\n' // "."
<< fs::path("/foo/..").filename() << '\n' // ".."
<< fs::path("//host").filename() << '\n'; // "host"
return 0;
}
#include <filesystem>
#include <iostream>
namespace fs = std ::filesystem;
int main() {
std::cout << fs::path("/foo/bar.txt").extension() << '\n' // ".txt"
<< fs::path("/foo/bar.").extension() << '\n' // "."
<< fs::path("/foo/bar").extension() << '\n' // ""
<< fs::path("/foo/bar.png").extension() << '\n' // ".png"
<< fs::path("/foo/.").extension() << '\n' // ""
<< fs::path("/foo/..").extension() << '\n' // ""
<< fs::path("/foo/.hidden").extension() << '\n' // ""
<< fs::path("/foo/..bar").extension() << '\n'; // ".bar"
return 0;
}
#include <filesystem>
#include <iostream>
namespace fs = std ::filesystem;
int main() {
std::cout << fs::path("/foo/bar.txt").stem() << std::endl // "bar"
<< fs::path("/foo/00000.png").stem() << std::endl // "00000"
<< fs::path("/foo/.bar").stem() << std::endl; // ".bar"
return 0;
}
#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std ::filesystem;
void demo_exists(const fs::path &p) {
std::cout << p;
if (fs::exists(p))
std::cout << " exists\n";
else
std::cout << " does not exist\n";
}
int main() {
fs::create_directory("sandbox");
std::ofstream("sandbox/file");
demo_exists("sandbox/file"); // "sandbox/file" exists
demo_exists("sandbox/cacho"); // "sandbox/cacho" does not exist
fs::remove_all("sandbox");
return 0;
}
-
bad – the unit is ambiguous
void blink_led_bad (int time_to_blink ) { // do something with time_to_blink }
-
good – the unit is explicit
void blink_led_good ( miliseconds time_to_blink ) { // do something with time_to_blink }
-
Usage
void use () { blink_led_good (100); // ERROR: What unit? blink_led_good (100ms); // blink_led_good (5s); // ERROR: Bad unit }
class Image { // Should be in Image.hpp
public:
Image(const std ::string &file_name);
void Draw();
private:
int rows_ = 0; // New in C+=11
int cols_ = 0; // New in C+=11
};
- struct is a class where everything is public
- GOOGLE-STYLE Use struct as a simple data container, if it needs a function it should be a class instead.
struct NamedInt {
int num;
std :: string name;
};
NamedInt var{1, std :: string{"hello"}};
- Classes always have at least one Constructor and exactly one Destructor
- Constructors crash course:
- Are functions with no explicit return type
- Named exactly as the class
- There can be many constructors
- If there is no explicit constructor an implicit default constructor will be generated.
- Destructor for class SomeClass:
~SomeClass()
- Last function called in the lifetime of an object
- Generated automatically if not explicitly defined
class SomeClass {
public:
SomeClass(){}; // Default constructor.
SomeClass(int a){}; // Custom constructor.
SomeClass(int a, float b){}; // Custom constructor.
~SomeClass(){}; // Destructor.
};
// How to use them?
int main() {
SomeClass var_1; // Default constructor
SomeClass var_2(10); // Custom constructor
// Type is checked when using {} braces. Use them!
SomeClass var_3{10}; // Custom constructor
SomeClass var_4 = {10}; // Same as var_3
SomeClass var_5{10, 10.0}; // Custom constructor
SomeClass var_6 = {10, 10.0}; // Same as var_5
return 0;
}
- Use initializer list to initialize data
- Name getter functions as the private member they return
- Avoid setters, set data in the constructor
class Student {
public:
// initializer list
Student(int id, std::string name) : id_{id}, name_{name} {}
// getter
int id() const { return id_; }
const std::string &name() const { return name_; }
private:
int id_;
std::string name_;
};
- Data members belong to declaration
- Class methods can be defined elsewhere
- Class name becomes part of function name
SomeClass::SomeClass () {} // This is a constructor int SomeClass::var () const { return var_; } void SomeClass::DoSmth () {}
- C++ 11 allows to initialize variables in-place
- Do not initialize them in the constructor
- No need for an explicit default constructor
class Student {
public:
// No need for default constructor.
// Getters and functions omitted.
private:
int earned_points_ = 0; // initialize in-place
float happiness_ = 1.0f;
};
- Note: Leave the members of structs uninitialized as which will forbid using brace initialization
- const after function states that this function does not change the object
int var () const;
- Mark all functions that should not change the state of the object as const
- Ensures that we can pass objects by a const reference and still call their functions
#include <iostream> #include <string> class Student { public: Student(std::string name) : name_(name) {} // This function might change the object // compilation error, you must add `const` after the function name() const std::string &name() { return name_; } private: std::string name_; }; void Print(const Student &s) { std::cout << s.name() << std::endl; } // error: 'this' argument to member function 'name' has type 'const Student', // but function is not marked const
- Every expression is an lvalue or an rvalue
- lvalues can be written on the left of assignment operator (=)
- rvalues are all the other expressions
- Explicit rvalue defined using &&
- Use
std::move(…)
to explicitly convert an lvalue to an rvalue
int a; // "a" is an lvalue
int& a_ref = a; // "a" is an lvalue
// "a_ref" is a reference to an lvalue
a = 2 + 2; // "a" is an lvalue ,
// "2 + 2" is an rvalue
int b = a + 2; // "b" is an lvalue ,
// "a + 2" is an rvalue
int&& c = std::move(a); // "c" is an rvalue
std::move
is used to indicate that an object t may be “moved from”, i.e. allowing the efficient transfer of resources from t to another object.- In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a
static_cast
to an rvalue reference type. - So, this is the definition, it's impossible to understand...
- The
std::move()
is a standard-library function returning an rvalue reference to its argument. std::move(x)
means "give me an rvalue reference to x.""- That is,
std::move(x)
does not move anything; instead, it allows a user to move x, taking ownership.
#include <iostream>
#include <string>
using namespace std; // Save space on slides.
void Print(const string &str) { cout << "lvalue: " << str << endl; }
// the parameter is a rvalue
void Print(string &&str) { cout << "rvalue: " << str << endl; }
int main() {
string hello = "hi";
Print(hello); // lvalue: hi
Print("world"); // rvalue: world
Print(std::move(hello)); // rvalue: hi
// DO NOT access "hello" after move!
return 0;
}
- The value after move is undefined
#include <iostream>
#include <string>
#include <vector>
int main() {
std::string str = "Hello";
std::vector<std::string> v;
// uses the push_back(const T&) overload , which means
// we'll incur the cost of copying str
v.push_back(str);
std::cout << "After copy , str is " << str << std::endl;
// After copy , str is Hello
// uses the rvalue reference push_back(T&&) overload ,
// which means no strings will be copied; instead ,
// the contents of str will be moved into the vector.
// This is less expensive , but also means str might
// now be empty.
v.push_back(std::move(str));
std::cout << "After move , str is " << str << std::endl;
// After move , str is
return 0;
}
- Think about ownership
- Entity owns a variable if it deletes it, e.g.
- A function scope owns a variable defined in it
- An object of a class owns its data members
- Moving a variable transfers ownership of its resources to another variable
- When designing your program think who should own this thing?.
- Runtime: better than copying, worse than passing by reference.
- Operators are functions with a signature:
<RETURN_TYPE> operator<NAME>(<PARAMS>)
<NAME>
represents the target operation, e.g.>, <, =, ==, <<
etc.- Have all attributes of functions
- All available operators: http://en.cppreference.com/w/cpp/language/operators
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
class Human {
public:
Human(int kindness) : kindness_(kindness) {}
bool operator<(const Human &other) const {
return kindness_ < other.kindness_;
}
int kindness() const { return kindness_; }
private:
int kindness_ = 100;
};
int main() {
std::vector<Human> humans = {Human(40), Human(20), Human(30)};
std::sort(humans.begin(), humans.end());
for (const auto &human : humans) {
std::cout << human.kindness() << std::endl;
}
// 20 30 40
return 0;
}
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
class Human {
public:
Human(int kindness) : kindness_(kindness) {}
int kindness() const { return kindness_; }
private:
int kindness_ = 100;
};
std::ostream &operator<<(std::ostream &os, const Human &human) {
return os << "Human with kindness " << human.kindness();
}
int main() {
std::vector<Human> humans = {Human{0}, Human{10}};
for (const auto &human : humans) {
std::cout << human << std::endl;
}
return 0;
}
// Human with kindness 0
// Human with kindness 10
- Called automatically when the object is copied
- For a class
MyClass
has the signature:MyClass(const MyClass& other)
MyClass a; // Calling default constructor. MyClass b(a); // Calling copy constructor. MyClass c = a; // Calling copy constructor.
- Copy assignment operator is called automatically when the object is assigned a new value from an Lvalue
- For class
MyClass
has a signature:MyClass& operator=(const MyClass& other)
- Returns a reference to the changed object
- Use this from within a function of a class to get a reference to the current object
MyClass a; // Calling default constructor. MyClass b(a); // Calling copy constructor. MyClass c = a; // Calling copy constructor. ??? a = b; // Calling copy assignment operator.
- Called automatically when the object is moved
- For a class
MyClass
has a signature:MyClass(MyClass&& other)
MyClass a; // Default constructors. MyClass b(std::move(a)); // Move constructor. MyClass c = std::move(a); // Move constructor.
- Called automatically when the object is assigned a new value from an Rvalue
- For class MyClass has a signature:
MyClass& operator=(MyClass&& other)
- Returns a reference to the changed object
MyClass a; // Default constructors. MyClass b(std::move(a)); // Move constructor. MyClass c = std::move(a); // Move constructor. b = std::move(c); // Move assignment operator.
#include <iostream>
using std::cout;
using std::endl;
class MyClass {
public:
MyClass() { cout << "default" << endl; }
// Copy(&) and Move(&&) constructors
MyClass(const MyClass &other) { cout << "copy" << endl; }
MyClass(MyClass &&other) { cout << "move" << endl; }
// Copy(&) and Move(&&) operators
MyClass &operator=(const MyClass &other) {
cout << "copy assignment" << endl;
return *this;
}
MyClass &operator=(MyClass &&other) {
cout << "move assignment" << endl;
return *this;
}
};
int main() {
MyClass a; // default
MyClass b = a; // copy
a = b; // copy assignment
MyClass c = std ::move(a); // move
c = std ::move(b); // move assignment
return 0;
}
- The constructors and operators will be generated automatically
- Under some conditions…
- Six special functions for class
MyClass
:MyClass() MyClass(const MyClass& other) MyClass& operator=(const MyClass& other) MyClass(MyClass&& other) MyClass& operator=(MyClass&& other) ~MyClass()
- None of them defined: all auto-generated
- Any of them defined: none auto-generated
- Try to define none of the special functions
- If you must define one of them define all
- Use
=default
to use default implementation- because usually the compiler will give you a better implementation
- for example, you have to define a destructor
class MyClass { public: MyClass () = default; MyClass(MyClass && var) = default; MyClass(const MyClass& var) = default; MyClass& operator=( MyClass && var) = default; MyClass& operator=(const MyClass& var) = default; };
- Any function can be set as deleted
void SomeFunc (...) = delete;
- Calling such a function will result in compilation error
- Example: remove copy constructors when only one instance of the class must be guaranteed (
Singleton Pattern
)
- Example: remove copy constructors when only one instance of the class must be guaranteed (
- Compiler marks some functions deleted automatically
- Example: if a class has a constant data member, the
copy/move
constructors andassignment
operators are implicitly deleted
- Example: if a class has a constant data member, the
- Static member variables of a class
- Exist exactly once per class, not per object
- The value is equal across all instances
- Must be defined in
*.cpp
files(before C++17)
- Static member functions of a class
- Do not need to access through an object of the class
- Can access private members but need an object
- Syntax for calling:
ClassName::MethodName(<params>)
- Use word
using
to declare new types from existing and to create type aliases - Basic syntax:
using NewType = OldType
; using
is a versatile word- When used outside of functions declares a new type alias
- When used in function creates an alias of a type available in the current scope
- http://en.cppreference.com/w/cpp/language/type_alias
#include <array>
#include <memory>
template <class T, int SIZE> struct Image {
// Can be used in classes.
using Ptr = std ::unique_ptr<Image<T, SIZE>>;
std ::array<T, SIZE> data;
};
// Can be combined with "template".
template <int SIZE> using Imagef = Image<float, SIZE>;
int main() {
// Can be used in a function for type aliasing.
using Image3f = Imagef<3>;
auto image_ptr = Image3f ::Ptr(new Image3f);
return 0;
}
- Store an enumeration of options
- Usually derived from
int
type - Options are assigned consequent numbers
- Mostly used to pick path in
switch
enum class EnumType { OPTION_1 , OPTION_2 , OPTION_3 };
- Use values as:
EnumType::OPTION_1, EnumType::OPTION_2, …
#include <iostream>
#include <string>
using std::cout, std::cerr, std::endl, std::string;
enum class Channel { STDOUT, STDERR };
void Print(Channel print_style, const string &msg) {
switch (print_style) {
case Channel::STDOUT:
cout << msg << endl;
break;
case Channel::STDERR:
cerr << msg << endl;
break;
default:
cerr << "Skipping\n";
}
}
int main() {
Print(Channel ::STDOUT, "hello");
Print(Channel ::STDERR, "world");
return 0;
}
- By default enum values start from 0
- We can specify custom values if needed
- Usually used with default values
enum class EnumType {
OPTION_1 = 10, // Decimal.
OPTION_2 = 0x2, // Hexacedimal.
OPTION_3 = 13
};
- C code
// "Base" class , Vehicle typedef struct vehicle { int seats_; // number of seats on the vehicle int capacity_ ; // amount of fuel of the gas tank char* brand_; // make of the vehicle } vehicle_t ;
- C++ code
class Vehicle { private: int seats_ = 0; // number of seats on the vehicle int capacity_ = 0; // amount of fuel of the gas tank string brand_; // make of the vehicle }
- Class and struct can inherit data and functions from other classes
- There are 3 types of inheritance in C++:
- public [used in this course] GOOGLE-STYLE
- protected
- private
- public inheritance keeps all access specifiers of the base class
- Allows Derived to use all public and protected members of Base
- Derived still gets its own special functions: constructors, destructor, assignment operators
class Derived : public Base { // Contents of the derived class. };
#include <iostream>
using std::cout, std::endl;
class Rectangle {
public:
Rectangle(int w, int h) : width_{w}, height_{h} {}
int width() const { return width_; }
int height() const { return height_; }
protected:
int width_ = 0;
int height_ = 0;
};
class Square : public Rectangle {
public:
// initialize base class with initializer list, same as Rectangle(w, h)
explicit Square(int size) : Rectangle{size, size} {}
};
int main() {
Square sq(10); // Short name to save space.
cout << sq.width() << " " << sq.height() << endl;
return 0;
}
- A function can be declared virtual:
virtual Func(<PARAMS>);
- If function is virtual in
Base
class it can be overridden inDerived
class:Func(<PARAMS>) override;
Base
can force allDerived
classes to override a function by making it pure virtualvirtual Func(<PARAMS>) = 0;
- sometimes, you may not see the keywords
virtual
, but it is still a pure virtual function, because- A member function that overrides a virtual funcction in the base class is automatically virtual even if the virtual keywors is not used
- Overloading
- Pick from all functions with the same name, but different parameters
- Pick a function at compile time
- Functions don’t have to be in a class
- overriding
- Pick from functions with the same arguments and names in different classes of one class hierarchy
- Pick at runtime
- Abstract class: class that has at least one
pure virtual function
- Interface: class that has only
pure virtual functions
and no data members
- A class with virtual functions has a virtual table
- When calling a function the class checks which of the virtual functions that match the signature should be called
- Called runtime polymorphism
- Costs some time but is very convenient
- Use interfaces when you must enforce other classes to implement some functionality
- Allow thinking about classes in terms of abstract functionality
- Hide implementation from the caller
- Allow to easily extend functionality by simply adding a new class
#include <iostream>
using std ::cout, std ::endl;
struct Printable { // Saving space. Should be a class.
virtual void Print() const = 0;
};
struct A : public Printable {
void Print() const override { cout << "A" << endl; }
};
struct B : public Printable {
void Print() const override { cout << "B" << endl; }
};
void Print(const Printable &var) { var.Print(); }
int main() {
Print(A());
Print(B());
return 0;
}
-
Allows morphing derived classes into their base class type:
const Base& base = Derived(…)
-
Allows encapsulating the implementation inside a class only asking it to conform to a common interface
-
Often used for:
- Working with all children of some Base class in unified manner
- Enforcing an interface in multiple classes to force them to implement some functionality
- In strategy pattern, where some complex functionality is outsourced into separate classes and is passed to the object in a modular fashion
-
GOOGLE-STYLE: Prefer composition
- i.e. including an object of another class as a member of your class
-
Every variable has a type
-
Types can be converted from one to another
-
Type conversion is called type casting
-
There are 5 ways of type casting:
static_cast
reinterpret_cast
const_cast
dynamic_cast
- C-style cast(unsafe)
- compile will try combination of those 4 casting, and you have no idea what's going on
static_cast<NewType>(variable)
- Convert type of a variable at compile time
- Rarely needed to be used explicitly
- Can happen implicitly for some types, e.g. float can be cast to int
- Pointer to an object of a Derived class can be upcast to a pointer of a Base class
- Enum value can be caster to int or float
- Full specification is complex!
dynamic_cast<Base*>(derived_ptr)
- Used to convert a pointer to a variable of Derived type to a pointer of a Base type
- Conversion happens at runtime
- If derived_ptr cannot be converted to
Base*
returns anullptr
- GOOGLE-STYLE Avoid using dynamic casting
reinterpret_cast<NewType>(variable)
- Reinterpret the bytes of a variable as another type
- We must know what we are doing!
- Mostly used when writing binary data
const_cast<NewType>(variable)
- Used to “constify” objects
- Used to “de-constify” objects
- Not widely used
GOOGLE-STYLE
Do not use C-style casts.GOOGLE-STYLE
Use brace initialization to convert arithmetic types (e.g.int64{x}
).- This is the safest approach because code will not compile if conversion can result in information loss. The syntax is also concise.
GOOGLE-STYLE
Usestatic_cast
as the equivalent of a C-style cast that does value conversion,- when you need to explicitly up-cast a pointer from a class to its superclass,
- or when you need to explicitly cast a pointer from a superclass to a subclass.
- In this last case, you must be sure your object is actually an instance of the subclass.
GOOGLE-STYLE
Useconst_cast
to remove the const qualifier (see const).google-style
usereinterpret_cast
to do unsafe conversions of pointer types to and from integer and other pointer types.- Use this only if you know what you are doing and you understand the aliasing issues.
- If a class relies on complex external functionality use strategy pattern
- Allows to add/switch functionality of the class without changing its implementation
- All strategies must conform to one strategy interface
#include <iostream>
using std::cout, std::endl;
class Strategy {
public:
virtual void Print() const = 0;
};
class StrategyA : public Strategy {
public:
void Print() const override { cout << "A" << endl; }
};
class StrategyB : public Strategy {
public:
void Print() const override { cout << "B" << endl; }
};
// so far, nothing is new, just interface
class MyClass {
public:
// 2. The strategy will be “picked” when
// we create an object of the class MyClass.
explicit MyClass(const Strategy &s) : strategy_(s) {}
void Print() const { strategy_.Print(); }
private:
// 1. holds a const reference to Strategy object
const Strategy &strategy_;
};
- Only use these patterns when you need to
- If your class should have a single method for some functionality and will never need another implementation don’t make it virtual
- Used mostly to avoid copying code and to make classes smaller by moving some functionality out.
- We want only one instance of a given class.
- Without C++ this would be a if/else mess.
- C++ has a powerfull compiler, we can use it.
- We can make sure that nobody creates more than 1 instance of a given class, at compile time.
- Don’t over use it, it’s easy to learn, but usually hides a design error in your code.
- Sometimes is still necessary, and makes your code better.
-
We can
delete
any class member functions. -
This also holds true for the special functions:
MyClass()
MyClass(const MyClass& other)
MyClass& operator=(const MyClass& other)
MyClass(MyClass&& other)
MyClass& operator=(MyClass&& other)
~MyClass()
-
Any
private
function can only be accessed by member of the class. -
Let’s hide the default Constructor and also the destructor.
class Singleton { private: Singleton () = delete ; ~ Singleton () = delete ; };
- This completely disable the possibility to create a Singleton object or destroy it.
-
And now let’s delete any copy capability:
- Copy Constructor.
- Copy Assigment Operator.
class Singleton { public: Singleton (const Singleton &) = delete; void operator=(const Singleton &) = delete; };
- This completely disable the possibility to copy any existing Singleton object.
- Now we need to create at least one instance of the
Singleton
class. - How? Compiler to the rescue:
- We can create one unique instance of the class.
- At compile time …
- Using
static
!.
class Singleton { public: static Singleton& GetInstance () { static Singleton instance ; return instance; } };
class Singleton {
private:
Singleton() = default;
~Singleton() = default;
public:
Singleton(const Singleton &) = delete;
void operator=(const Singleton &) = delete;
static Singleton &GetInstance() {
static Singleton instance;
return instance;
}
};
int main() {
auto &singleton = Singleton ::GetInstance();
// ...
// do stuff with singleton , the only instance.
// ...
Singleton s1; // Compiler Error!
Singleton s2(singleton); // Compiler Error!
Singleton s3 = singleton; // Compiler Error!
return 0;
}
- cdtdebug
- You actually don't do this. If you do, then there's something else wrong...
MyClass* obj_ptr = &obj; obj_ptr ->MyFunc ();
obj->Func()
↔(*obj).Func()
- Pointers are just like references, but have additional useful properties:
- Can be reassigned
- Can point to “nothing” (
nullptr
) - Can be stored in a vector or an array
- Use pointers for polymorphism
Derived derived; Base* ptr = &derived;
- Every object of a class or a struct holds a pointer to itself
- This pointer is called
this
- Allows the objects to:
- Return a reference to themselves: *return this;
- Create copies of themselves within a function
- Explicitly show that a member belongs to the current object:
this->x();
this
is a C++ keyword
- Pointers can point to a
const
variable:// Cannot change value , can reassign pointer. const MyType* const_var_ptr = &var; const_var_ptr = & var_other ;
- Pointers can be const:
// Cannot reassign pointer , can change value. MyType* const var_const_ptr = &var; var_const_ptr ->a = 10;
- Pointers can do both at the same time:
// Cannot change in any way, read -only. const MyType* const const_var_const_ptr = &var;
- Read from right to left to see which const refers to what
- Static memory (compile time)
- Available for short term storage (scope)
- Small / limited (8 MB Linux typically)
$ ulimit -a -t: cpu time (seconds) unlimited -f: file size (blocks) unlimited -d: data seg size (kbytes) unlimited -s: stack size (kbytes) 8192 ...
- Memory allocation is fast
- LIFO (Last in First out) structure
- Items added to top of the stack with
push
- Items removed from the top with
pop
- Dynamic memory (runtime)
- Available for long time (program runtime)
- Raw modifications possible with
new
anddelete
(usually encapsulated within a class) - Allocation is slower than stack allocations
- User controls memory allocation (unsafe)
- Use
new
to allocate data:// pointer variable stored on stack int* int_ptr = nullptr; // 'new' returns a pointer to memory in heap int_ptr = new int; // also works for arrays float* float_ptr = nullptr; // 'new' returns a pointer to an array on heap float_ptr = new float[number ];
new
returns an address of the variable on the heap- Prefer using smart pointers!
- Memory is not freed automatically!
- User must remember to free the memory
- Use
delete
ordelete[]
to free memory:int* int_ptr = nullptr; int_ptr = new int; // delete frees memory to which the pointer points delete int_ptr; // also works for arrays float* float_ptr = nullptr; float_ptr = new float[number ]; // make sure to use 'delete[]' for arrays delete[] float_ptr ;
- Prefer using smart pointers!
- Can happen when working with Heap memory if we are not careful
- Memory leak: memory allocated on Heap access to which has been lost
heap ptr_1 ----> ▢▢▢▢▢▢▢▢▢ / LEAKED! ptr_2 / ▨▨▨▨▨▨▨▨▨
- It will also raise a problem of double free.
- Dangling Pointer: pointer to a freed memory
- Think of it as the opposite of a memory leak
- Dereferencing a dangling pointer causes undefined behavior
int* ptr_1 = some_heap_address ;
int* ptr_2 = some_heap_address ;
delete ptr_1;
ptr_1 = nullptr;
// Cannot use ptr_2 anymore! Behavior undefined!
-
Resource Allocation Is Initialization.
-
New object → allocate memory
-
Remove object → free memory
-
Objects own their data!
-
You say, ah, I know how to RAII now!
class MyClass { public: MyClass() { data_ = new SomeOtherClass; } ~MyClass() { delete data_; data_ = nullptr; } private: SomeOtherClass *data_; };
-
Does it work ?
- No!
- Still cannot copy an object of
MyClass
!!!
int main() { MyClass a; MyClass b(a); // !! double free or corruption : 0 x0000000000877c20 *** return 0; }
- Shallow copy: just copy pointers, not data
- Deep copy: copy data, create new pointers
- Default copy constructor and assignment operator implement shallow copying
- RAII + shallow copy → dangling pointer
- RAII + Rule of All Or Nothing → correct
- Use smart pointers instead!
- Smart pointers wrap a raw pointer into a class and manage its lifetime (RAII)
- Smart pointers are all about ownership
- Always use smart pointers when the pointer should own heap memory
- Only use them with heap memory!.
- Still use raw pointers for non-owning pointers and simple address storing
#include <memory>
to use smart pointers
- std::unique_ptr
- std::shared_ptr
- std::auto_ptr
- std::weak_ptr
- We will focus on 2 types of smart pointers:
std::unique_ptr
,std::shared_ptr
- Smart pointers apart from memory allocation behave exactly as raw pointers(it is still poiner):
- Can be set to
nullptr
- Use
*ptr
to dereferenceptr
- Use
ptr->
to access methods - Smart pointers are polymorphic
- Can be set to
- Additional functions of smart pointers:
ptr.get()
returns a raw pointer that the smart pointer managesptr.reset(raw_ptr)
stops using currently managed pointer, freeing its memory if needed, setsptr
toraw_ptr
- A Simple Example
- Create an
unique_ptr
to a typeVehicle
std :: unique_ptr <Vehicle> vehicle_1 = std :: make_unique <Bus>(20 , 10, "Volkswagen", "LPM_"); std :: unique_ptr <Vehicle> vehicle_2 = std :: make_unique <Car>(4, 60, "Ford", "Sony");
- Now you can have fun as we had with raw pointers
// vehicle_x is a pointer , so we can us it as it is vehicle_1 ->Print (); vehicle_2 ->Print ();
- Create an
-
unique_ptr
are unique (ownship is unique): This means that we can move stuff but not copy:vehicle_2 = std :: move( vehicle_1 );
-
Address of the pointers before the move:
cout << "vehicle_1 = " << vehicle_1 .get () << endl; cout << "vehicle_2 = " << vehicle_2 .get () << endl; // vehicle_1 = 0x56330247ce70 (xxx70) // vehicle_2 = 0x56330247cec0 (xxxc0)
-
Address of the pointers after the move:
// vehicle_2 = 0 x56330247ce70 // vehicle_1 = 0
-
Wait, Didn't you use NEW and DELETE ?
- No, we are smart now... right ?
- Constructor of a unique pointer takes ownership of a provided raw pointer
- No runtime overhead over a raw pointer
- Syntax for a unique pointer to type Type: (Ugly!!)
#include <memory> // Using default constructor Type(); auto p = std :: unique_ptr <Type>(new Type); // Using constructor Type(<params>); auto p = std :: unique_ptr <Type>(new Type(<params>));
- From C++14 on:
// Forwards <params> to constructor of unique_ptr auto p = std :: make_unique <Type>(<params>);
- Unique pointer has no copy constructor
- Cannot be copied, can be moved
- Guarantees that memory is always owned by a single
std::unique_ptr
- A non-null
std::unique_ptr
always owns what it points to. - Moving a
std::unique_ptr
transfers ownership from the source pointer to the destination pointer.- (The source pointer is set to
nullptr
.)
- (The source pointer is set to
-
What if we want to use the same
pointer
for different resources? -
An object accessed via
std::shared_ptrs
has its lifetime managed by those pointers through shared ownership. -
No specific
std::shared_ptr
owns the object.- because you just share the ownship
-
When the last
std::shared_ptr
pointing to an object stops pointing there, thatstd::shared_ptr
destroys the object it points to -
Constructed just like a
unique_ptr
-
Can be copied
-
Stores a usage counter and a raw pointer
- Increases usage counter when copied
- Decreases usage counter when destructed
-
Frees memory when counter reaches 0
-
Can be initialized from a
unique_ptr
-
Syntax:
#include <memory> // Using default constructor Type(); auto p = std :: shared_ptr <Type>(new Type); auto p = std :: make_shared <Type>(); // Using constructor Type(<params>); auto p = std :: shared_ptr <Type>(new Type(<params>)); auto p = std :: make_shared <Type>(<params>);
#include <iostream>
using std::cout, std::endl;
class MyClass {
public:
MyClass() { cout << "I'm alive!\n"; }
~MyClass() { cout << "I'm dead... :(\n"; }
};
int main() {
auto a_ptr = std ::make_shared<MyClass>();
cout << a_ptr.use_count() << endl;
{
auto b_ptr = a_ptr;
cout << a_ptr.use_count() << endl;
}
cout << "Back to main scope\n";
cout << a_ptr.use_count() << endl;
return 0;
}
// I'm alive!
// 1
// 2
// Back to main scope
// 1
// I'm dead... :(
- Use smart pointers when the pointer must manage memory
- By default use
unique_ptr
- If multiple objects must share ownership over something, use a
shared_ptr
to it - Think of any free standing
new
ordelete
as of a memory leak or a dangling pointer:- Don’t use
delete
- Allocate memory with
make_unique
,make_shared
- Only use
new
in smart pointer constructor if cannot use the functions above
- Don’t use
int main () {
// Allocate a variable in the stack
int a = 42;
// Create a pointer to that part of the memory
// first error: why you create a pointer to stack variable? just use reference!
int* ptr_to_a = &a;
// Know stuff about pointers eh?
// 2nd error
auto a_unique_ptr = std :: unique_ptr <int>( ptr_to_a);
// Same happens with std::shared_ptr.
// 3rd error
auto a_shared_ptr = std :: shared_ptr <int>( ptr_to_a);
std :: cout << "Program terminated correctly!!!\n";
return 0;
}
- Create a smart pointer from a
pointer
to a stack-managed variable - The variable ends up being owned both by the
smart pointer
and thestack
and gets deleted twice → Error!
template <typename T>
T abs(T x) {
return (x>= 0) ? x : -x;
}
- Function templates are not functions.
- They are templates for making functions
- You don’t pay for what you don’t use:
- If nobody calls
abs<int>
, it won’t be instantiated by the compiler at all.
- If nobody calls
- A function template defines a family of functions.
int main () {
const double x = 5.5;
const int y = -5;
auto abs_x = abs <double>(x);
int abs_y = abs <int>(y);
double abs_x_2 = abs(x); // type -deduction
auto abs_y_2 = abs(y); // type -deduction
}
Templates lives in a "static" world.
template <class T> class MyClass {
public:
MyClass(T x) : x_(x) {}
private:
T x_;
};
- Classes templates are not classes.
- They are templates for making classes
- You don’t pay for what you don’t use:
- If nobody calls
MyClass<int>
, it won’t be instantiated by the compiler at all.
- If nobody calls
int main() {
MyClass<int> my_float_object(10);
MyClass<double> my_double_object(10.0);
return 0;
}
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
template <typename T, size_t N = 10>
T AccumulateVector (const T& val) {
std::vector <T> vec(val , N);
return std::accumulate (vec.begin (), vec.end (), 0);
}
- Every template is parameterized by one or more template parameters:
template < parameter-list > declaration
- Think the template parameters the same way as any function arguemnents, but at compile-time.
using std::cout, std::endl;
int main() {
cout << AccumulateVector(1) << endl; // 10
cout << AccumulateVector<float>(2) << endl; // 20
cout << AccumulateVector<float, 5>(2.0) << endl; // 10
return 0;
}
TODO
constexpr
specifies that the value of a variable or function can appear in constant expressions
#include <iostream>
constexpr int factorial(int n) {
// Compute this at compile time
return n <= 1 ? 1 : (n * factorial(n - 1));
}
int main() {
// Guaranteed to be computed at compile time
return factorial(10);
}
- It only works if the variable of function can be defined at compile-time:
#include <array> #include <vector> int main() { std ::vector<int> vec; constexpr size_t size_err = vec.size(); // error std ::array<int, 10> arr; constexpr size_t size = arr.size(); // works! }