Author: Hayden Smith
Why?
What?
What is an object in C++?
An object is a region of memory associated with a type
Unlike some other languages (Java), basic types such as int and bool are objects
For the most part, C++ objects are designed to be intuitive to use
What special things can we do with objects
Create
Destroy
Copy
Move
auto okay(int& i) -> int& {
return i;
}
auto okay(int& i) -> int const& {
return i;
}
auto not_okay(int i) -> int& {
return i;
}
auto not_okay() -> int& {
auto i = 0;
return i;
}
Objects are either stored on the stack or the heap
In general, most times you've been creating objects of a type it has been on the stack
We can create heap objects via new and free them via delete just like in C (malloc/free)
New and delete call the constructors/destructors of what they are creating
#include <iostream>
#include <vector>
int main() {
int* a = new int{4};
std::vector<int>* b = new std::vector<int>{1,2,3};
std::cout << *a << "\n";
std::cout << (*b)[0] << "\n";
delete a;
delete b;
return 0;
}
demo501-new.cpp
Why do we need heap resources?
Heap object outlives the scope it was created in
More useful in contexts where we need more explicit control of ongoing memory size (e.g. vector as a dynamically sized array)
Stack has limited space on it for storage, heap is much larger
#include <iostream>
#include <vector>
int* newInt(int i) {
int* a = new int{i};
return a;
}
int main() {
int* myInt = newInt();
std::cout << *a << "\n"; // a was defined in a scope that
// no longer exists
delete a;
return 0;
}
demo502-scope.cpp
Let's speculate about how a vector is implemented. It's going to have to manage some form of heap memory, so maybe it looks like this? Is anything wrong with this?
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Destructor
~my_vec() {};
int* data_;
int size_;
int capacity_;
}
my_vec::~my_vec() {
delete[] data_;
}
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Destructor
~my_vec() {};
int* data_;
int size_;
int capacity_;
}
When writing a class, if we can't default all of our operators (preferred), we should consider the "rule of 5"
The presence or absence of these 5 operations are critical in managing resources
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
// Call constructor.
auto vec_short = my_vec(2);
auto vec_long = my_vec(9);
// Doesn't do anything
auto& vec_ref = vec_long;
// Calls copy constructor.
auto vec_short2 = vec_short;
// Calls copy assignment.
vec_short2 = vec_long;
// Calls move constructor.
auto vec_long2 = std::move(vec_long);
// Calls move assignment
vec_long2 = std::move(vec_short);
class my_vec {
// Constructor
my_vec(int size):
data_{new int[size]},
size_{size},
capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_short2 = vec_short;
my_vec::my_vec(my_vec const& orig): data_{new int[orig.size_]},
size_{orig.size_},
capacity_{orig.size_} {
std::copy(orig.data_, orig.data_ + orig.size_, data_);
}
class my_vec {
// Constructor
my_vec(int size):
data_{new int[size]},
size_{size},
capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_long = my_vec(9);
vec_long = vec_short;
my_vec& my_vec::operator=(my_vec const& orig) {
my_vec(orig).swap(*this); return *this;
}
void my_vec::swap(my_vec& other) {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
std::swap(capacity_, other.capacity_);
}
// Alternate implementation, may not be as performant.
my_vec& my_vec::operator=(my_vec const& orig) {
my_vec copy = orig;
std::swap(copy, *this);
return *this;
}
int main() {
int i = 5; // 5 is rvalue, i is lvalue
int j = i; // j is lvalue, i is lvalue
int k = 4 + i; // 4 + i produces rvalue
// then stored in lvalue k
}
void f(my_vec& x);
void inner(std::string&& value) {
value[0] = 'H';
std::cout << value << '\n';
}
void outer(std::string&& value) {
inner(value); // This fails? Why?
std::cout << value << '\n';
}
int main() {
outer("hello"); // This works fine.
auto s = std::string("hello");
inner(s); // This fails because s is an lvalue.
}
void f(my_vec&& x);
// Looks something like this.
T&& move(T& value) {
return static_cast<T&&>(value);
}
void inner(std::string&& value) {
value[0] = 'H';
std::cout << value << '\n';
}
void outer(std::string&& value) {
inner(std::move(value));
// Value is now in a valid but unspecified state.
// Although this isn't a compiler error, this is bad code.
// Don't access variables that were moved from, except to reconstruct them.
std::cout << value << '\n';
}
int main() {
f1("hello"); // This works fine.
auto s = std::string("hello");
f2(s); // This fails because i is an lvalue.
}
class my_vec {
// Constructor
my_vec(int size)
: data_{new int[size]}
, size_{size}
, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_short2 = std::move(vec_short);
my_vec::my_vec(my_vec&& orig) noexcept
: data_{std::exchange(orig.data_, nullptr)}
, size_{std::exchange(orig.size_, 0)}
, capacity_{std::exchange(orig.capacity_, 0)} {}
Very similar to copy constructor, except we can use std::exchange instead.
Like the move constructor, but the destination is already constructed
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_long = my_vec(9);
vec_long = std::move(vec_short);
my_vec& my_vec::operator=(my_vec&& orig) noexcept {
// The easiest way to write a move assignment is generally to do
// memberwise swaps, then clean up the orig object.
// Doing so may mean some redundant code, but it means you don't
// need to deal with mixed state between objects.
std::swap(data_, orig.data_);
std::swap(size_, orig.size_);
std::swap(capacity_, orig.capacity_);
// The following line may or may not be nessecary, depending on
// if you decide to add additional constraints to your moved-from object.
delete[] orig.data_
orig.data_ = nullptr;
orig.size_ = 0;
orig.capacity = 0;
return *this;
}
class T {
T(const T&) = delete;
T(T&&) = delete;
T& operator=(const T&) = delete;
T& operator=(T&&) = delete;
};
In summary, today is really about emphasising RAII
Resource = heap object
A concept where we encapsulate resources inside objects
eg. Memory, locks, files
Every resource should be owned by either:
Another resource (eg. smart pointer, data member)
Named resource on the stack
A nameless temporary variable
To create safe object lifetimes in C++, we always attach the lifetime of one object to that of something else