CS106L课程笔记
Stanford CS106L: Standard C++ Programming课程笔记。
引言:CS106B/X 和 CS106L 是配套课程,学习完前一个再学习 CS106L 才是正确的路径。但是浙江大学的《数据结构基础》课程已经包括了 CS106B 中除 C++ Class 和 Huffman Coding 之外的其他内容,所以对于 CS106B 的内容仅做简单补充。
此笔记基于 CS 106L, Fall ‘21
Lec1 Welcome to CS 106L!
Why C++ is important
What is C++
1 |
|
1 |
|
Lec2 Types and Structs
Types make things better…and sometimes harder…but still better
Types
Fundamental Types
1 | int val = 5; //32 bits |
C++ is a statically typed language: everything with a name (variables, functions, etc) is given a type before runtime
static typing helps us to prevent errors before our code runs
Static Types + Function
1 | int add(int a, int b); |
Overloading
1 | int half(int x, int divisor = 2) { // (1) |
Intro to structs
struct: a group of named variables each with their own type. A way to bundle different types together
1 | struct Student { |
std::pair: An STL built-in struct with two fields of any type
1 | std::pair<int, string> numSuffix = {1,"st"}; |
auto: Keyword used in lieu of type when declaring a variable, tells the compiler to deduce the type.
1 | //It means that the type is deduced by the compiler. |
Sneak peek at streams
stream: an abstraction for input/output. Streams convert between data and the string representation of data.
1 | std::cout << 5 << std::endl; // prints 5 |
Recap
- Everything with a name in your program has a type
- Strong type systems prevent errors before your code runs!
- Structs are a way to bundle a bunch of variables of many types
- std::pair is a type of struct that had been defined for you and is in the STL
- So you access it through the std:: namespace (std::pair)
- auto is a keyword that tells the compiler to deduce the type of a variable, it should be used when the type is obvious or very cumbersome to write out
Lec3 Initialization & References
Initialization
Initialization: How we provide initial values to variables
1 | // Recall: Two ways to initialize a struct |
1 | //Multiple ways to initialize a pair |
1 | //Initialization of vectors |
Uniform initialization: curly bracket initialization. Available for all types, immediate initialization on declaration(统一初始化:声明时用花括号定义)
1 | std::vector<int> vec{1,3,5}; |
auto: use it to reduce long type names
1 | std::pair<bool, std::pair<double, double>> result = quadratic(a, b, c); |
Don’t overuse auto!
1 | //A better way to use quadratic |
References
Reference: An alias (another name) for a named variable
References in 106B
1 | void changeX(int& x){ //changes to x will persist |
References in 106L: References to variables
1 | vector<int> original{1, 2}; |
Reference-copy bug
1 | //bug |
- l-values
- l-values can appear on the left or right of an =
x
is an l-value- l-values have names
- l-values are not temporary
- r-values
- r-values can ONLY appear on the right of an =
3
is an r-value- r-values don’t have names
- r-values are temporary
The classic reference-rvalue error
1 | //可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值; |
BONUS: Const and Const References
const indicates a variable can’t be modified!
1 | std::vector<int> vec{1, 2, 3}; |
1 | const std::vector<int> c_vec{7, 8}; // a const variable |
const & subtleties
1 | std::vector<int> vec{1, 2, 3}; |
Remember: C++, by default, makes copies when we do variable assignment! We need to use & if we need references instead.
Recap
- Use input streams to get information
- Use structs to bundle information
- Use uniform initialization wherever possible
- Use references to have multiple aliases to the same thing
- Use const references to avoid making copies whenever possible
Lec4 Streams
stream: an abstraction for input/output. Streams convert between data and the string representation of data.
Input streams
std::cin is an input stream. It has type std::istream
- Have type std::istream
- Can only receive strings using the
>>
operator- Receives a string from the stream and converts it to data
- std::cin is the input stream that gets input from the console
1 | int x; |
- First call to std::cin >> creates a command line prompt that allows the user to type until they hit enter
- Each
>>
ONLY reads until the next whitespace- Whitespace = tab, space, newline
- Everything after the first whitespace gets saved and used the next time std::cin >> is called
- If there is nothing waiting in the buffer, std::cin >> creates a new command line prompt
- Whitespace is eaten: it won’t show up in output
1 | string str; |
To read a whole line, use std::getline(istream& stream, string& line);
1 | std::string line; |
>>
reads up to the next whitespace character and does not go past that whitespace character.getline
reads up to the next delimiter (by default, ‘\n’), and does go past that delimiter.
Output streams
std::cout is an output stream. It has type std::ostream
- Can only send data using the << operator
- Converts any type into string and sends it to the stream
- std::cout is the output stream that goes to the console
File streams
Input File Streams
- Have type std::ifstream
- Only send data using the >> operator
- Receives strings from a file and converts it to data of any type
- Must initialize your own ifstream object linked to your file
1 | std::ifstream in(“out.txt”); |
Output File Streams
- Have type std::ofstream
- Only send data using the << operator
- Converts data of any type into a string and sends it to the file stream
- Must initialize your own ofstream object linked to your file
1 | std::ofstream out(“out.txt”); |
string streams
- Input stream: std::istringstream
- Give any data type to the istringstream, it’ll store it as a string!
- Output stream: std::ostringstream
- Make an ostringstream out of a string, read from it word/type by word/type
- The same as the other i/ostreams you’ve seen!
ostringstreams
1 | string judgementCall(int age, string name, bool lovesCpp) |
istringstreams
1 | Student reverseJudgementCall(string judgement){ |
Recap
- Streams convert between data of any type and the string representation of that data.
- Streams have an endpoint: console for cin/cout, files for i/o fstreams, string variables for i/o streams where they read in a string from or output a string to.
- To send data (in string form) to a stream, use stream_name << data.
- To extract data from a stream, use stream_name >> data, and the stream will try to convert a string to whatever type data is.
Lec5 Containers
What’s in the STL:
- Containers
- Iterators
- Functions
- Algorithms
Types of containers
All containers can hold almost all elements
1 | graph TB |
1 | graph TB |
Sequence Containers
vector
1 |
|
array
1 |
|
deque
deque
支持vector
的所有操作,并且支持快速push_front()
,但是实践中一般使用vector
,因为其他操作更快。
list
A list provides fast insertion anywhere, but no random (indexed) access.
What you want to do | std::vector | std::deque | std::list |
---|---|---|---|
Insert/remove in the front | Slow | Fast | Fast |
Insert/remove in the back | Super Fast | Very Fast | Fast |
Indexed Access | Super Fast | Fast | Impossible |
Insert/remove in the middle | Slow | Fast | Very Fast |
Memory usage | Low | High | High |
Combining (splicing/joining) | Slow | Very Slow | Fast |
Stability (iterators/concurrency) | Bad | Very Bad | Good |
wrapper: A wrapper on an object changes how external users can interact with that object.
Container adaptors are wrappers in C++!
queue
1 | queue.push_back(); |
stack
1 | stack.push_back(); |
priority_queue
Adding elements with a priority, always removing the highest priority-element.
Associative Containers
set
set
就是集合,每个元素只出现一次,按键值升序排列。访问元素的时间复杂度是O(logn).
1 | std::set<int> s;//Create an empty set |
map
map
是c++标准库中定义的关联容器,是键(key)值(value)对的结合体。
1 | std::map<int, char> m;//Create an empty map |
Every std::map<k, v>
is actually backed by: std::pair<const k, v>
1 | //Iterating through maps and sets |
unordered_map and unordered_set
- Each STL set/map comes with an unordered sibling. They’re almost the same, except:
- Instead of a comparison operator, the set/map type must have a hash function defined for it.
- Simple types, like int, char, bool, double, and even std::string are already supported!
- Any containers/collections need you to provide a hash function to use them.
- Instead of a comparison operator, the set/map type must have a hash function defined for it.
- unordered_map/unordered_set are generally faster than map/set.
Recap
- Sequence Containers
std::vector
- use for almost everythingstd::deque
- use when you need fast insertion to front AND back
- Container Adaptors
sta::stack
andstd::queue
- Associative Containers
std::map
andstd::set
- if using simple data types/you’re familiar with hash functions, use
std::unordered_map
andstd::unordered_set
Lec6 Iterators and Pointers
Iterators
A way to access all containers programmatically!
- Iterators are objects that point to elements inside containers.
- Each STL container has its own iterator, but all of these iterators exhibit a similar behavior!
- Generally, STL iterators support the following operations:
1 | std::set<type> s = {0, 1, 2, 3, 4}; |
Types:
- Input Iterator:只能单步向前迭代元素,不允许修改由该类迭代器引用的元素。
- Output Iterator:该类迭代器和Input Iterator极其相似,也只能单步向前迭代元素,不同的是该类迭代器对元素只有写的权力。
- Forward Iterator:该类迭代器可以在一个正确的区间中进行读写操作,它拥有Input Iterator的所有特性,和Output Iterator的部分特性,以及单步向前迭代元素的能力。
- Bidirectional Iterator:该类迭代器是在Forward Iterator的基础上提供了单步向后迭代元素的能力。
- Random Access Iterator:该类迭代器能完成上面所有迭代器的工作,它自己独有的特性就是可以像指针那样进行算术计算,而不是仅仅只有单步向前或向后迭代。
Explain:
There are a few different types of iterators, since containers are different!
All iterators can be incremented (++)
Input iterators can be on the RHS (right hand side) of an = sign:
auto elem = *it;
Output iterators can be on the LHS of = :
*elem = value;
Random access iterators support indexing by integers!
1
2
3it += 3; // move forward by 3
it -= 70; // move backwards by 70
auto elem = it[5]; // offset by 5
Why ++iter and not iter++?
Answer: ++iter returns the value after being incremented! iter++ returns the previous value and then increments it. (wastes just a bit of time)
1 | std::map<int, int> map {{1, 2}, {3, 4}}; |
1 | std::set<int> set{3, 1, 4, 1, 5, 9}; |
1 | std::set<int> set{3, 1, 4, 1, 5, 9}; |
1 | auto key = (*iter).first; |
Pointers
- When variables are created, they’re given an address in memory.
- Pointers are objects that store an address and type of a variable.
- To get the value of a pointer, we can dereference it (get the object referenced by the pointer)
1 | int x = 5; |
Pointers vs. Iterators
- Iterators are a form of pointers!
- Pointers are more generic iterators
- can point to any object, not just elements in a container!
1 | std::string lands = "Xadia"; |
Lec7 Classes
Containers are all classes defined in the STL!
Iterators are (basically) pointers! More on that later
Class: A programmerdefined custom type. An abstraction of an object or data type.
But don’t structs do that?
1 | struct Student { |
Issues with structs
- Public access to all internal state data by default
- Users of struct need to explicitly initialize each data member.
Classes provide their users with a public interface and separate this from a private implementation.
Turning Student into a class: Header File + .cpp File:
1 | //student.h |
Function definitions with namespaces!
namespace_name::name in a function prototype means “this is the implementation for an interface function in namespace_name ”
Inside the {…} the private member variables for namespace_name will be in scope!
std::string Student::getName(){...}
The this keyword!
Here, we mean “set the Student private member variable name equal to the parameter name ”
1
2
3void Student::setName(){
name = name;
}this->element_name means “the item in this Student object with name element_name”. Use this for naming conflicts!
1
2
3void Student::setName(string name){
this->name = name; //better!
}
Constructors and Destructors
constructors:
- Define how the member variables of an object is initialized
- What gets called when you first create a Student object
- Overloadable!
destructors:
- deleteing (almost) always happens in the destructor of a class!
- The destructor is defined using Class_name::~Class_name()
- No one ever explicitly calls it! Its called when Class_name object go out of scope!
- Just like all member functions, declare it in the .h and implement in the .cpp!
构造函数就是一个与类名相同的函数,在生成这个类的时候就会被调用,用来初始化这个类。
与构造函数相对的是析构函数,在关闭文件、释放内存前释放资源,名称是类名前加一个~
1 |
|
Public and Private Sections
Class: A programmerdefined custom type. An abstraction of an object or data type.
1 | //student.h |
Public section:
- Users of the Student object can directly access anything here!
- Defines interface for interacting with the private member variables!
Private section:
- Usually contains all member variables
- Users can’t access or modify anything in the private section
One last thing… Arrays
1 | //int * is the type of an int array variable |
Lec8 Template Classes and Const Correctness
Template Classes
Fundamental Theorem of Software Engineering: Any problem can be solved by adding enough layers of indirection.
The problem with IntVector
Vectors should be able to contain any data type!
Solution? Create StringVector, DoubleVector, BoolVector etc..
What if we want to make a vector of struct Students?
- How are we supposed to know about every custom class?
What if we don’t want to write a class for every type we can think of?
SOLUTION: Template classes!
Template Class: A class that is parametrized over some number of types. A class that is comprised of member variables of a general type/types.
Template Classes You’ve Used
Vectors/Maps/Sets… Pretty much all containers!
1 | template<class T> |
Writing a Template Class: Syntax
1 | //mypair.h |
Member Types
Sometimes, we need a name for a type that is dependent on our template types
iterator is a member type of vector
1
2std::vector a = {1, 2};
std::vector::iterator it = a.begin();
Summary:
- Used to make sure your clients have a standardized way to access important types.
- Lives in your namespace:
vector<T>::iterator
. - After class specifier, you can use the alias directly (e.g. inside function arguments, inside function body).
- Before class specifier, use typename.
1 | // main.cpp |
Templates don’t emit code until instantiated, so include the .cpp in the .h instead of the other way around!
Const Correctness
const: keyword indicating a variable, function or parameter can’t be modified
const indicates a variable can’t be modified!
1 | std::vector<int> vec{1, 2, 3}; |
Can’t declare non-const reference to const variable!
1 | const std::vector<int> c_vec{7, 8}; // a const variable |
const & subtleties with auto
1 | std::vector<int> vec{1, 2, 3}; |
Why const?
1 | // Find the typo in this code |
1 | // Overly ambitious functions in application code |
Calling addLittleHat on p is like setting a non const variable equal to a const one, it’s not allowed!
Const and Classes
1 | //student.cpp |
Using a const Student:
1 | //main.cpp |
- The compiler doesn’t know getName and getAge don’t modify s!
- We need to promise that it doesn’t by defining them as const functions
- Add const to the end of function signatures!
So, we make Student const-correct:
1 | //student.cpp |
const-interface: All member functions marked const in a class definition. Objects of type const ClassName may only use the const-interface.
Making RealVector‘s const-interface:
1 | class StrVector { |
Should begin() and end() be const?
Answer: 虽然这两个函数都是const的,但是它们给我们返回了一个可以变化的iterator,所以会报错!
Solution: cbegin() and cend()
1 | class StrVector { |
1 | void printVec(const RealVector& vec){ |
const iterator vs const_iterator: Nitty Gritty
1 | using iterator = std::string*; |
Recap
Template classes
- Add
template<typename T1, typename T2 ...>
before class definition in .h - Add
template<typename T1, typename T2 ...>
before all function signature in .cpp - When returning nested types (like iterator types), put
template<typename T1, typename T2 ...>::member_type
as return type, not justmember_type
- Templates don’t emit code until instantiated, so
#include
the .cpp file in the .h file, not the other way around
Const and Const-correctness
- Use const parameters and variables wherever you can in application code
- Every member function of a class that doesn’t change its member variables should be marked
const
- auto will drop all const and &, so be sure to specify
- Make iterators and const_iterators for all your classes!
- const iterator = cannot increment the iterator, can dereference and change underlying value
- const_iterator = can increment the iterator, cannot dereference and change underlying value
- const const_iterator = cannot increment iterator, cannot dereference and change underlying value
Lec9 Template Functions
Generic Programming
Generic C++
- Allow data types to be parameterized (C++ entities that work on any datatypes)
- Template classes achieve generic classes
- How can we write methods that work on any data type?
Function to get the min of two ints
1 | int myMin(int a, int b) { |
One solution: overloaded functions
1 | int myMin(int a, int b) { |
But what about comparing other data types, like doubles, characters, and complex objects?
Template functions
Writing reusable, unique code with no duplication!
1 | //generic, "template" functions |
Template type deduction - case 1
If the template function parameters are regular, pass-by-value parameters:
- Ignore the “&”
- After ignoring “&”, ignore const too
1 | template <typename Type> |
Template type deduction - case 2
If the template function parameters are references or pointers, this is how types (e.g. Type) are deduced:
- Ignore the “&”
- Match the type of parameters to inputted arguments
- Add on const after
1 | template <typename Type> |
behind the scenes
Normal functions are created during compile time, and used in runtime
Template functions are not compiled until used by the code
1
2
3
4
5template <typename Type>
Type myMin(Type a, Type b) {
return a < b ? a : b;
}
cout << myMin(3, 4) << endl; // 3The compiler deduces the parameter types and generates a unique function specifically for each time the template function is called
After compilation, the compiled code looks as if you had written each instantiated version of the function yourself
Template Metaprogramming
- Normal code runs during run time.
- TMP -> run code during compile time
- make compiled code packages smaller
- speed up code when it’s actually running
1 | template<unsigned n> |
How can TMP actually be used?
- TMP was actually discovered (not invented, discovered) recently!
- Where can TMP be applied
- Ensuring dimensional unit correctness
- Optimizing matrix operations
- Generating custom design pattern implementation
- policy-based design (templates generating their own templates)
Why write generic functions?
1 | Count the |
Counting Occurrences
1 | //Attempt 1 |
1 | //Attempt 2 |
1 | //Attempt 3 |
1 | //Attempt 4 |
Lec10 Functions and Lambdas
Review of template functions
1 | template <typename InputIt, typename DataType> |
Could we reuse this to find how many vowels are in ”Xadia”, or how many odd numbers were in a std::vector?
Function Pointers and Lambdas
Predicate Functions
Any function that returns a boolean is a predicate!
1 | //Unary Predicate |
Function Pointers for generalization
1 | template <typename InputIt, typename UnaryPred>//no typename DataType |
isVowel is a pointer, just like Node *
or char *
! It’s called a “function pointer”, and can be treated like a variable.
Function pointers don’t generalize well.
Lambdas
1 | auto var = [capture-clause] (auto param) -> bool { |
capture clause
1 | [] // captures nothing |
Solution
1 | template <typename InputIt, typename UniPred> |
what really are they
- Lambdas are cheap, but copying them may not be.
- Use lambdas when you need a short function, or one with read/write access to local variables
- Use function pointers for longer logic and for overloading
- We use “auto” because type is figured out in compile time
Functors and Closures
1 | class functor { |
- A functor is any class that provides an implementation of operator().
- Lambdas are essentially syntactic sugar for creating a functor.
- If lambdas are functor classes, then “closures” are instances of those classes.
- At runtime, closures are generated as instances of lambda classes.
How do functors, lambdas, and function pointers relate?
Answer: standard function, std::function<…>, is the one to rule them all — it’s the overarching type for anything callable in C++. Functors, lambdas, and function pointers can all be casted to standard functions
1 | void functionPointer (int arg) { |
We could cast either functionPointer or lambda to func, as both of them have a void return signature and take in one integer parameter.
Introducing STL Algorithms
A collection of completely generic functions written by C++ devs
#include <algorithm>
:
sort · reverse · min_element · max_element · binary_search · stable_partition · find · find_if · count_if · copy · transform · insert · for_each · etc.!
Lec11 Operator Overloading
Redefining what operators mean
Function Overloading
Allow for calling the same function with different parameters:
1 | int sum(int a, int b) { |
Operator Overloading
\+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= *= /= %= ^= &= |= <<= >>= [] () -> ->* new new[] delete delete[]
1 | if (before(a, b)) { // a, b defined earlier |
Two ways to overload operators
Member Functions
Add a function called operator __ to your class:
1 | class Time { |
- Call the function on the left hand side of the expression (this)
- Binary operators (5 + 2, “a” < “b”): accept the right hand side (& rhs) as an argument(参数).
- Unary operators (~a, !b): don’t take any arguments
1 | class Time { |
- Operators can only be called on the left hand side
- What if we can’t control what’s on the left hand side of the operation?
- e.g. if we want to compare a double and a Fraction
Non-Member Functions
Add a function called operator __ outside of your class:
1 | bool operator < (const Time& lhs, const Time& rhs); |
Instead of taking only rhs, it takes both the left hand side and right hand side!
The STL prefers using non-member functions for operator overloading:
- allows the LHS to be a non-class type (e.g. double < Fraction)
- allows us to overload operations with a LHS class that we don’t own
You may be wondering how non-member functions can access private member variables:
The answer: friends!
1 | class Time { |
<< Operator Overloading
We can use << to output something to an std::ostream&:
1 | std::ostream& operator << (std::ostream& out, const Time& time) { |
This is how std::cout mixes types (and still works)!
1 | //Since these two methods are implemented in the STL |
Don’t overuse operator overloading!
1 | //Confusing |
Rules of Operator Overloading
- Meaning should be obvious when you see it
- Should be reasonably similar to corresponding arithmetic operations
- Don’t define
+
to mean set subtraction!
- Don’t define
- When the meaning isn’t obvious, give it a normal name instead
Lec12 Special Member Function
Special Member Functions (SMFs)
These functions are generated only when they’re called (and before any are explicitly defined by you):
- Default Constructor
- Copy Constructor
- Copy Assignment Operator
- Destructor
- Move Constructor
- Move Assignment Operator
1 | class Widget { |
- Default Constructor
- object is created with no parameters
- constructor also has no parameters
- all SMFs are public and inline function, meaning that wherever it’s used is replaced with the generated code in the function
- Copy Constructor
- another type of constructor that creates an instance of a class
- constructs a member-wise copy of an object (deep copy)
- Copy Assignment Operator
- very similar to copy constructor, except called when trying to set one object equal to another e.g. w1 = w2;
- Destructor
- called whenever object goes out of scope
- can be used for deallocating member variables and avoiding memory leaks
- Move Constructor
- Move Assignment Operator
1 | //Examples: |
Copy Constructors and Copy Assignment Operators
initializer lists
1 | template <typename T> |
- Prefer to use member initializer lists, which directly constructs each member with a given value
- Faster! Why construct, and then immediately reassign?
- What if members are a non-assignable type (you’ll see by the end of lecture how this can be possible!)
- Important clarification: you can use member initializer lists for ANY constructor, even if it has parameters (and thus isn’t an SMF)
Why aren’t the default SMFs always sufficient?
The default compiler-generated copy constructor and copy assignment operator functions work by manually copying each member variable!
Moral of the story: in many cases, copying is not as simple as copying each member variable!
1 | //the default copy constructor |
1 | //the default copy assignment operator |
Copy operations must perform these tasks:
- Copy constructor
- Use initializer list to copy members where simple copying does the correct thing.
- int, other objects, etc
- Manually copy all members otherwise
- pointers to heap memory
- non-copyable things
- Use initializer list to copy members where simple copying does the correct thing.
- Copy assignment
- Clean up any resources in the existing object about to be overwritten
- Copy members using direct assignment when assignment works
- Manually copy members where assignment does not work
- You don’t have to do these in this order
Summary: Steps to follow for an assignment operator
- Check for self-assignment.
- Make sure to free existing members if applicable.
- Copy assign each automatically assignable member.
- Manually copy all other members.
- Return a reference to *this (that was just reassigned).
= delete and = default
1 | //Explicitly delete the copy member functions |
1 | //Is there a way to keep, say, the default copy constructor if you write another constructor? |
Rule of 0 and Rule of 3
Rule of 0
If the default operations work, then don’t define your own!
When should you define your own SMFs
- When the default ones generated by the compiler won’t work
- Most common reason: there’s a resource that our class uses that’s not stored inside of our class
- e.g. dynamically allocated memory
- our class only stores the pointers to arrays, not the arrays in memory itself
- e.g. dynamically allocated memory
Rule of 3 (C++ 98)
- If you explicitly define a copy constructor, copy assignment operator, or destructor, you should define all three
- What’s the rationale?
- If you’re explicitly writing your own copy operation, you’re controlling certain resources manually
- You should then manage the creation, use, and releasing of those resources!
Recap of Special Member Functions (SMFs)
- Default Constructor
- Object created with no parameters, no member variables instantiated
- Copy Constructor
- Object created as a copy of existing object (member variable-wise)
- Copy Assignment Operator
- Existing object replaced as a copy of another existing object.
- Destructor
- Object destroyed when it is out of scope.
Are these 4 enough?
1 | class StringTable { |
Move constructors and move assignment operators
Move Operations (C++11)
These functions are generated only when they’re called (and before any are explicitly defined by you)
1 | //Allow for moving objects and std::move operations (rvalue refs) |
- Move constructors and move assignment operators will perform “memberwise moves”
- Defining a copy constructor does not affect generation of a default copy assignment operator, and vice versa
- Defining a move assignment operator prevents generation of a move copy constructor, and vice versa
- Rationale: if the move assignment operator needs to be re-implemented, there’d likely be a problem with the move constructor
Some nuances to move operation SMFs
- Move operations are generated for classes only if these things are true:
- No copy operations are declared in the class
- No move operations are declared in the class
- No destructor is declared in the class
- Can get around all of these by using default:
1 | Widget(Widget&&) = default; |
Lec13 Move Semantics in C++
l-values live until the end of the scope
r-values live until the end of the line
1 | //Find the r-values! (Only consider the items on the right of = signs) |
How many arrays will be allocated, copied and destroyed here?
1 | int main() { |
- vec is created using the default constructor
- make_me_a_vec creates a vector using the default constructor and returns it
- vec is reassigned to a copy of that return value using copy assignment
- copy assignment creates a new array and copies the contents of the old one
- The original return value’s lifetime ends and it calls its destructor
- vec’s lifetime ends and it calls its destructor
How do we know when to use move assignment and when to use copy assignment?
Answer: When the item on the right of the = is an r-value we should use move assignment
Why? r-values are always about to die, so we can steal their resources
1 | //Examples |
the r-value reference
How to make two different assignment operators? Overload vector::operator= !
How? Introducing… the r-value reference
&&
(This is different from the l-value reference & you have see before) (it has one more ampersand)
Overloading with &&
1 | int main() { |
Copy assignment and Move assignment
1 | //Copy assignment |
The compiler will pick which vector::operator= to use based on whether the RHS is an l-value or an r-value
Can we make it even better?
In the move assignment above, these are also making copies (using int/ptr copy assignment)
1 | _size = other._size; |
We can force move assignment rather than copy assignment of these ints by using std::move
1 | vector<T>& operator=(vector<T>&& other) { |
The compiler will pick which vector::operator= to use based on whether the RHS is an l-value or an r-value
Constructor
1 | //How about this |
1 | //copy constructor |
Where else should we use std::move?
Answer:
- Wherever we take in a const & parameter in a class member function and assign it to something else in our function
- Don’t use std::move outside of class definitions, never use it in application code!
vector::push_back
1 | //Copy push_back |
Be careful with std::move
1 | int main() { |
- After a variable is moved via std::move, it should never be used until it is reassigned to a new variable!
- The C++ compiler might warn you about this mistake, but the code above compiles!
TLDR: Move Semantics
- If your class has copy constructor and copy assignment defined, you should also define a move constructor and move assignment
- Define these by overloading your copy constructor and assignment to be defined for Type&& other as well as Type& other
- Use std::move to force the use of other types’ move assignments and constructors
- All std::move(x) does is cast x as an rvalue
- Be wary of std::move(x) in main function code
Bonus: std::move and RAII
- Recall: RAII means all resources required by an object are acquired in its constructor and destroyed in its destructor
- To be consistent with RAII, you should have no half-ready resources, such as a vector whose underlying array has been deallocated
Is std::move consistent with RAII?
- I say NO!
- This is a sticky language design flaw, C++ has a lot of those!
Lec14 Type Safety and std::optional
Recap: Const-Correctness
- We pass big pieces of data by reference into helper functions by to avoid making copies of that data
- If this function accidentally or sneakily changes that piece of data, it can lead to hard to find bugs!
- Solution: mark those reference parameters const to guarantee they won’t be changed in the function!
How does the compiler know when it’s safe to call member functions of const variables?
const-interface: All member functions marked const in a class definition. Objects of type const ClassName may only use the const-interface.
RealVector’s const-interface
1 | template<class ValueType> class RealVector { |
Key Idea: Sometimes less functionality is better functionality
- Technically, adding a const-interface only limits what RealVector objects marked const can do
- Using types to enforce assumptions we make about function calls help us prevent programmer errors!
Type Safety
Type Safety: The extent to which a language prevents typing errors.guarantees the behavior of programs.
What does this code do?
1 | void removeOddsFromEnd(vector<int>& vec){ |
1 | //One solution |
There may or may not be a “last element” in vec. How can vec.back() have deterministic behavior in either case?
The problem
1 | valueType& vector<valueType>::back(){ |
Type Safety: The extent to which a function signature guarantees the behavior of a function.
The problem
1 | //back() is promising to return something of type valueType when its possible no such value exists! |
std::optional
What is std::optional<T>
?
std::optional
is a template class which will either contain a value of type T or contain nothing (expressed as nullopt
)
1 | void main(){ |
What if back() returned an optional?
1 | std::optional<valueType> vector<valueType>::back(){ |
std::optional interface
.value()
returns the contained value or throws bad_optional_access error.value_or(valueType val)
returns the contained value or default value, parameter val.has_value()
returns true if contained value exists, false otherwise
Checking if an optional has value
1 | std::optional<Student> lookupStudent(string name){//something} |
So we have perfect solutions
1 | void removeOddsFromEnd(vector<int>& vec){ |
Recap: The problem with std::vector::back()
- Why is it so easy to accidentally call back() on empty vectors if the outcome is so dangerous?
- The function signature gives us a false promise!
- Promises to return an something of type valueType
- But in reality, there either may or may not be a “last element” in a vector
std::optional “monadic” interface (C++23 sneak peek!)
.and_then(function f)
returns the result of calling f(value) if contained value exists, otherwise null_opt (f must return optional).transform(function f)
returns the result of calling f(value) if contained value exists, otherwise null_opt (f must return optional).or_else(function f)
returns value if it exists, otherwise returns result of calling f
Recall: Design Philosophy of C++
- Only add features if they solve an actual problem
- Programmers should be free to choose their own style
- Compartmentalization is key
- Allow the programmer full control if they want it
- Don’t sacrifice performance except as a last resort
- Enforce safety at compile time whenever possible
Recap: Type Safety and std::optional
- You can guarantee the behavior of your programs by using a strict type system!
- std::optional is a tool that could make this happen: you can return either a value or nothing
- .has_value()
- .value_or()
- .value()
- This can be unwieldy and slow, so cpp doesn’t use optionals in most stl data structures
- Many languages, however, do!
- The ball is in your court!
“Well typed programs cannot go wrong.”
- Robert Milner (very important and good CS dude)
Lec15 RAII, Smart Pointers, and C++ Project Building
Exceptions - Why care?
How many code paths are in this function?
1 | string get_name_and_print_sweet_tooth(Person p) { |
- Code Path 1 - favors neither chocolate nor milkshakes
- Code Path 2 - favors milkshakes
- Code Path 3 - favors chocolate (and possibly milkshakes)
Are there any more code paths?
Hint: Exceptions
Exceptions are ways to signal that something has gone wrong during run-time
Exceptions are “thrown” and can crash the program, but can be “caught” to avoid this
Hidden Code Paths
There are (at least) 23 code paths in the code before!
- (1) copy constructor of Person parameter may throw
- (5) constructor of temp string may throw
- (6) call to favorite_food, favorite_drink, first (2), last (2), may throw
- (10) operators may be user-overloaded, thus may throw
- (1) copy constructor of string for return value may throw
What could go wrong here?
1 | string get_name_and_print_sweet_tooth(int id_number) { |
This problem isn’t just unique to pointers
Acquire | Release | |
---|---|---|
Heap memory | new | delete |
Files | open | close |
Locks | try_lock | unlock |
Cockets | socket | close |
How do we guarantee resources get released, even if there are exceptions?
RAII
Resource Acquisition Is Initialization
What is R·A·Double I?
- All resources used by a class should be acquired in the constructor
- All resources used by a class should be released in the destructor
Why?
- Objects should be usable immediately after creation
- There should never be a “half-valid” state of an object, where it exists in memory but is not accessible to/used by the program
- The destructor is always called (when the object goes out of scope), so the resource is always freed
Is it RAII Compliant?
1 | //The following three algorithms are not RALL |
This fixes it!
1 | void cleanDatabase (mutex& databaseLock, map<int, int>& database) { |
What about RAII for memory?
This is where we’re going with RAII: from the C++ Core Guidelines:
Avoid calling new
and delete
explicitly
Smart Pointers
RAII for memory
We saw how this was not RAII-compliant because of the “naked” delete.
1 | string get_name_and_print_sweet_tooth(int id_number) { |
Solution: built-in “smart” (RAII-safe) pointers
- Three types of smart pointers in C++ that automatically free underlying memory when destructed
- std::unique_ptr • Uniquely owns its resource, can’t be copied
- std::shared_ptr • Can make copies, destructed when underlying memory goes out of scope
- std::weak_ptr • models temporary ownership: when an object only needs to be accessed if it exists (convert to shared_ptr to access)
std::unique_ptr
1 | //Before |
what if we wanted to have multiple pointers to the same object? std::shared_ptr
std::shared_ptr
- Resources can be stored by any number of shared_ptrs
- The resource is deleted when none of the pointers points to the resource
1 | { |
Smart pointers: RAII Wrapper for pointers
1 | std::unique_ptr<T> up{new T}; |
1 | //So which way is better? |
- If we don’t use make_shared, then we’re allocating memory twice (once for sp, and once for new T)!
- We should be consistent across smart pointers
Building C++ Projects
What happens when you run our “./build_and_run.sh”?
What do make and Makefiles do?
- make is a “build system”
- uses g++ as its main engine
- several stages to the compiler system
- can be utilized through a Makefile!
- let’s take a look at a simple makefile to get some practice!
So why do we use cmake in our assignments?
- cmake is a cross-platform make
- cmake creates build systems!
- It takes in an even higher-level config file, ties in external libraries, and outputs a Makefile, which is then run.
- Let’s take a look at our makefiles!
Example cmake file (CMakeLists.txt)
1 | cmake_minimum_required(VERSION 3.0) # 指定 cmake 最低版本 |
Components of C++’s compilation system
1 | graph LR |
Preprocessing (g++ -E)
- The C/C++ preprocessor handles preprocessor directives: replaces includes (#include …) and and expands any macros (#define …)
- Replace #includes with content of respective files (which is usually just function/variable declarations, so low bloat)
- Replaces macros (#define) and selecting different portions of text depending on #if, #ifdef, #ifndef
- Outputs a stream of tokens resulting from these transformations
- If you want, you can produce some errors at even this stage (#if, #error)
Compilation (g++ -S)
- Performed on output of the preprocessor (full C++ code)
- Structure of a compiler:
- Lexical Analysis
- Parsing
- Semantic Analysis
- Optimization
- Code Generation (assembly code)
- This is where traditional “compiler errors” are caught
Assembling (g++ -c)
- Runs on the assembly code as outputted by the compiler
- Converts assembly code to binary machine code
- Assumes that all functions are defined somewhere without checking
- Final output: object files
- Can’t be run by themselves!
Linking (ld, g++)
- Creates a single executable file from multiple object files
- Combine the pieces of a program
- Figure out a memory organization so that all the pieces can fit together
- Resolve references so that the program can run under the new memory organization
- .h files declare functions, but the actual functions may be in separate files from where they’re called!
- Output is fully self-suficient—no other files needed to run