template: titleslide
template: titleslide
-
User defined types.
-
can be defined with either the
classorstructkeyword.
struct Complex {
double re;
double im;
};- Creating trivial types - give the class name then list the values to be assigned to the members, in order, inside braces:
Complex mk_imaginary_unit() {
return Complex{0, 1};
}This is called aggregate initialisation.
Often you want to control the creation of instances of your classes.
You do this with constructors - these are special member "functions" with the same name as the type.
struct Complex {
Complex() = default;
Complex(double re);
Complex(double re, double im);
double re = 0.0;
double im = 0.0;
};Note we declare three:
- one that initialises with a purely real value
- one that initialises with a real and imaginary value
- a default constructor which needs no arguments (that we tell the
compiler to generate for us as before with
= default)
???
Control in more detail than just starting from a default value or having to provide all the of member values.
Constructors are not strictly functions in C++ but very nearly (next slide)
Why do you have to "explictly default the default constructor"?
Because the language rules say if the user provides any constructors, the compiler must not create one unless asked to...
- Constructors are not directy callable
- Constructors do not return a value
- Constructors can do initialisation of member variables before the body begins execution
Let's define the ones we declared just now:
Complex::Complex(double real) : re{real} {
}
Complex::Complex(double real, double imag) : re{real}, im{imag} {
}You can also control what happens when your objects reach the end of their lifetime.
When this happens is deterministic:
- when a local variable goes out of scope
- when an object that contains them is destroyed
- when the programmer
deletes them
For a class Name they are declared like:
struct Name {
~Name();
};It's important to note that you should never call this directly - the compiler will call it for you when your objects are deallocated.
???
Note the tilde syntax is the logical negation of the class. Cf annihilation operators for any physicists.
A very important pattern in C++ is RAII: resource allocation is instantiation.
Also known as constructor acquires, destructor releases (CADRe).
This odd name is trying to communicate that any resource you have should be tied to the lifetime of an object.
So when the compiler destroys your object, it will release the resource (e.g. memory).
???
Saying that in some philosophical sense allocating a resource is the creation of something, which implies its destruction later.
A very simple copy of std::vector<double>:
class my_array {
unsigned size = 0;
double* data = nullptr;
public:
my_array() = default;
explicit my_array(unsigned n) : size(n), data(new double[size]) {}
~my_array() {
delete[] data;
}
double& operator[](unsigned i) {
return data[i];
}
};???
This class allocates some memory to store n doubles when constructed
When it reaches the end of its life the destructor returns the memory to the OS
It allows users to access elements (with no bounds checking)
Add a few annotations to print in the contructor/destructor
???
Open sample/arr1.cpp Compile and run
What happens if we copy x?
Add auto x_cp = x; (same as auto x_cp = my_array{x};)
When you assign a value to an object in C++, it will only be valid if there is a copy constructor or copy assignment operator
--
Copy constructor - when you create a new object as the destination:
my_array x{10}; // Direct initialisation
my_array y{x}; // Direct initialisation
my_array z = x; // Copy initialization-- Copy assignment - when you assign a new value to an existing object
my_array x{10};
x = my_array{2000};???
What's the diff?
In the last case, you have to deal with releasing any resources held by the target object
The compiler will automatically generate these operations for us if all the data members of you class are copyable.
So what went wrong with the example shown?
-- A pointer is just a number and so it can be copied implicitly - hence the double delete
If we want to copy our array then we need to either:
- copy the data (aka deep copy)
- share the data and somehow keep track of when the last reference to it is destroyed (aka shallow copy)
???
Deep copies are more expensive at run time but somewhat safer
Shallow copies can be faster but harder to implement correctly and can have thread safety issues
Do we want to copy?
Of course, you can control how your objects are copied
class my_array {
unsigned size = 0;
double* data = nullptr;
public:
my_array() = default;
explicit my_array(unsigned n) : size(n) data(new double[size]) {}
my_array(my_array const& other) : size(other.size), data(new double[size]) {
// Copy data
}
my_array& operator=(my_array const& other) {
delete[] data;
size = other.size;
data = new double[size];
// Copy data
return *this;
}
~my_array() {
delete[] data;
}
};???
Open arr2.cpp
Note the signature
When a function returns a value, you might think that will copy it to the target:
std::vector<int> ReadData() {
std::vector<int> answer;
// Read it from somewhere
return answer;
}
int main() {
auto data = ReadData();
}???
Thinking about std::vector examples we've seen and that you might have implemented
Have previously said that you should use bare auto when you want a copy - by that what we really mean is you want to own the object and control its lifetime.
Copying a vector of billions of elements is going to get expensive and would be counter to C++'s zero overhead abstractions principle
Since C++11, the language has supported the concept of moving from objects which the compiler knows (or the programmer asserts) will not be used any more.
Examples are:
- temporaries (i.e. the result of a function call/constructor expression)
- automatic variables that are going out of scope
- the result of calling
std::moveon an object
The destination object "steals" the contained resources from the source object and sets the source to a valid but undefined state - typically the only operations you can perform on a moved-from object are destruction and assignment.
Going back to our simple array:
class my_array {
unsigned size = 0;
double* data = nullptr;
public:
// c'tors, copy assignment, d'tor
my_array(my_array&& other) noexcept : size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
}
my_array& operator=(my_array&& other) noexcept {
std::swap(size, other.size);
std::swap(data, other.data);
}
};???
Comment on noexcept - this is for STL compatibility. The containers
will copy if your move operations are not noexcept. These ones cannot
throw exceptions so this is safe.
Look at arr3.cpp
This says that if you define or delete one of the following:
- copy constructor
- copy assignment operator
- move constructor
- move assignment operator
- destructor
then you should probably do so for all five.
??? This can be quite a lot of work!
This says that unless your class solely deals with ownership, then it should define none of the five special functions.
This is really a corollary of the general software engineering "principle of single responsibility".
You should split your code into a resource manager with all five functions and a user class that has none, but uses the resource manager as one or more data members.
???
If it does deal with ownership then rule of 5 applies :(
In your clone of this repository, find the 6.1-my-array exercise and list the files:
$ cd archer2-cpp/exercises/6.1-my-array
$ ls
Makefile README.md my_array.hpp part1.cpp part2.cpp part3.cpp test.cppmy_array.hpp contains a partial implementation of an array class. part1.cpp, part2.cpp, and part3.cpp contain examples using the my_array class that you can use to check your implementation and understanding.
.columns[ .col15[ Part 1 ] .col75[
- Implement the constructor and the destructor ] ]
.columns[ .col15[ Part 2 ] .col75[
- Implement the copy constructor
- Implement the copy assignment operator ] ]
.columns[ .col15[ Part 3 ] .col75[
- Implement the move constructor
- Implement the move assignment operator ] ]