To create safe object lifetimes in C++, we always attach the lifetime of one object to that of something else
We need to be very careful when returning references.
The object must always outlive reference.
auto okay(int& i) -> int& {
return i;
}
auto okay(int& i) -> int const& {
return i;
}
auto questionable(int const& x) -> int const& {
return i;
}
auto not_okay(int i) -> int& {
return i;
}
auto not_okay() -> int& {
auto i = 0;
return i;
}
// myintpointer.h
class MyIntPointer {
public:
// This is the constructor
MyIntPointer(int* value);
// This is the destructor
~MyIntPointer();
int* GetValue();
private:
int* value_;
};
// myintpointer.cpp
#include "myintpointer.h"
MyIntPointer::MyIntPointer(int* value): value_{value} {}
int* MyIntPointer::GetValue() {
return value_
}
MyIntPointer::~MyIntPointer() {
// Similar to C's free function.
delete value_;
}
Don't use the new / delete keyword in your own code
We are showing for demonstration purposes
void fn() {
// Similar to C's malloc
MyIntPointer p{new int{5}};
// Copy the pointer;
MyIntPointer q{p.GetValue()};
// p and q are both now destructed.
// What happens?
}
Type | Shared ownership | Take ownership |
---|---|---|
std::unique_ptr<T> | No | Yes |
raw pointers | No | No |
std::shared_ptr<T> | Yes | Yes |
std::weak_ptr<T> | No | No |
Also note the use of 'nullptr' in C++ instead of NULL
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> up1{new int};
std::unique_ptr<int> up2 = up1; // no copy constructor
std::unique_ptr<int> up3;
up3 = up2; // no copy assignment
up3.reset(up1.release()); // OK
std::unique_ptr<int> up4 = std::move(up3); // OK
std::cout << up4.get() << "\n";
std::cout << *up4 << "\n";
std::cout << *up1 << "\n";
}
Can we remove "new" completely?
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> up1(new int{0});
*up1 = 5;
std::cout << *up1 << "\n";
int* op1 = up1.get();
*op1 = 6;
std::cout << *op1 << "\n";
up1.reset();
std::cout << *op1 << "\n";
}
#include <memory>
#include <iostream>
int main() {
// 1 - Worst - you can accidentally own the resource multiple
// times, or easily forget to own it.
int *i = new int;
auto up1 = std::make_unique<std::string>(i);
auto up11 = std::make_unique<std::string>(i);
// 2 - Not good - requires actual thinking about whether there's a leak.
std::unique_ptr<std::string> up2{new std::string{"Hello"}};
// 3 - Good - no thinking required.
std::unique_ptr<std::string> up3 = std::make_unique<std::string>("Hello");
std::cout << *up3 << "\n";
std::cout << *(up3.get()) << "\n";
std::cout << up3->size();
}
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> x(new int{5});
std::shared_ptr<int> y = x; // Both now own the memory
std::cout << "use count: " << x.use_count() << "\n";
std::cout << "value: " << *x << "\n";
x.reset(); // Memory still exists, due to y.
std::cout << "use count: " << y.use_count() << "\n";
std::cout << "value: " << *y << "\n";
y.reset(); // Deletes the memory, since
// no one else owns the memory
std::cout << "use count: " << x.use_count() << "\n";
std::cout << "value: " << *y << "\n";
}
Can we remove "new" completely?
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> x = std::make_shared<int>(1);
std::weak_ptr<int> wp = x; // x owns the memory
{
std::shared_ptr<int> y = wp.lock(); // x and y own the memory
if (y) {
// Do something with y
std::cout << "Attempt 1: " << *y << '\n';
}
} // y is destroyed. Memory is owned by x
x.reset(); // Memory is deleted
std::shared_ptr<int> z = wp.lock(); // Memory gone; get null ptr
if (z) {
// will not execute this
std::cout << "Attempt 2: " << *z << '\n';
}
}
void g() {
throw std::runtime_error{""};
}
int main() {
auto ptr = new int{5};
g();
// Never executed.
delete ptr;
}
void g() {
throw std::runtime_error{""};
}
int main() {
auto ptr = std::make_unique<int>(5);
g();
}
Not safe
Safe
Resource acquisition is initialisation
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)
The stack
A nameless temporary variable
#include <exception>
class my_int {
public:
my_int(int const i) : i_{i} {
if (i == 2) {
throw std::exception();
}
}
private:
int i_;
};
class unsafe_class {
public:
unsafe_class(int a, int b)
: a_{new my_int{a}}
, b_{new my_int{b}}
{}
~unsafe_class() {
delete a_;
delete b_;
}
private:
my_int* a_;
my_int* b_;
};
int main() {
auto a = unsafe_class(1, 2);
}
Spot the bug
#include <exception>
#include <memory>
class my_int {
public:
my_int(int const i)
: i_{i} {
if (i == 2) {
throw std::exception();
}
}
private:
int i_;
};
class safe_class {
public:
safe_class(int a, int b)
: a_(std::make_unique<my_int>(a))
, b_(std::make_unique<my_int>(b))
{}
private:
std::unique_ptr<my_int> a_;
std::unique_ptr<my_int> b_;
};
int main() {
auto a = safe_class(1, 2);
}
We learnt about references to objects in Week 1.
We learnt about containers, iterators, and views in Week 2.
We've just learnt about smart pointers in Week 4.
How do they all tie together and what's the relationship with pointers?
class string_view6771 {
public:
string_view6771() noexcept = default;
explicit(false) string_view6771(std::string const& s) noexcept
: string_view6771(s.data())
{}
explicit(false) string_view6771(char const* data) noexcept
: string_view6771(data, std::strlen(data))
{}
string_view6771(char const* data, std::size_t const length) noexcept
: data_{data}
, length_{length}
{}
auto begin() const noexcept -> char const* { return data_; }
auto end() const noexcept -> char const* { return data_ + length_; }
auto size() const noexcept -> std::size_t { return length_; }
auto data() const noexcept -> char const* { return data_; }
auto operator[](std::size_t const n) const noexcept -> char {
assert(n < length_);
return data_[n];
}
private:
char const* data_ = nullptr;
std::size_t length_ = 0;
};
A reference type is a type that acts as an abstraction over a raw pointer.
They don't own a resource, but allow us to do cheap operations like copy, move, and destroy (see Week 5) on objects that do.
A reference type to a range of elements is called a view.
Abstraction over immutable string-like data.
Abstraction over array-like data.
std::span<T const>
ranges::views::*
Raw pointer to a single object or to an element in a range. Wherever possible, prefer references, iterators, or something below.
T const*
Lazy abstraction over a range, associated with some transformation.
Most
auto zero_out(std::span<int> const x) -> void {
ranges::fill(x, 0);
}
{
auto numbers = views::iota(0, 100) | ranges::to<std::vector>;
zero_out(numbers);
CHECK(ranges::all_of(numbers, [](int const x) { return x == 0; }));
}
{
// Using int[] since spec requires we use T[] instead of std::vector
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
auto const raw_data = std::make_unique<int[]>(42);
auto const usable_data = std::span<int>(raw_data.get(), 42);
zero_out(usable_data);
CHECK(ranges::all_of(usable_data, [](int const x) { return x == 0; }));
}
NOLINTNEXTLINE(check) turns off a check on the following line.
Do this rarely, and always provide justification immediately above.
auto zero_exists(std::span<int const> const x) -> bool {
ranges::any_of(x, [](int const x) { return x == 0; });
}
{
auto numbers = views::iota(0, 100) | ranges::to<std::vector>;
CHECK(zero_exists(numbers));
}
{
// Using int[] since ass2 spec requires we use T[] instead of std::vector
// NOLINTNEXTLINE(modernize-avoid-c-arrays)
auto const raw_data = std::make_unique<int[]>(42);
auto const usable_data = std::span<int>(raw_data.get(), 42);
CHECK(zero_exists(usable_data));
}
NOLINTNEXTLINE(check) turns off a check on the following line.
Do this rarely, and always provide justification immediately above.
A pointer is an abstraction of a virtual memory address.
Iterators are a generalization of pointers that allow a C++ program to work with different data structures... in a uniform manner.
Iterators are a family of concepts that abstract different aspects of addresses, ...
We need to be very careful when using reference types.
The owner must always outlive the observer.
auto v = std::vector<int>{0, 1, 2, 3};
auto const* p = v.data();
{
CHECK(*p == 0); // okay: p points to memory
// owned by v
}
v = std::vector<int>{0, 1, 2, 3};
{
CHECK(*p == 0); // error: p points to memory
// owned by no one
}
We need to be very careful when using reference types.
The owner must always outlive the observer.
auto not_okay1(std::string s) -> std::string_view { // s is a local; will be destroyed
return s; // before its observer
} // s destroyed here
auto not_okay2(std::string const& s) -> std::string_view { // s may be destroyed before observer;
return s; // considered harmful
} // e.g. auto x = not_okay("hello")
auto okay1(std::string_view const sv) -> std::string_view { // observer in, observer out
return sv;
}
auto okay2(std::string& s) -> std::string_view { // lvalue reference in, observer out
return s;
}
We need to be very careful when using reference types.
The owner must always outlive the observer.
auto is_even(int const x) -> bool {
return x % 2 == 0;
};
auto not_okay1(std::vector<int> v) {
return v | views::filter(is_even);
} // v destroyed here
auto not_okay2(std::vector<int> const& v) {
return v | views::filter(is_even);
}
auto okay1(std::span<int> s) {
return s | views::filter(is_even);
}
auto okay2(std::vector<int>& v) {
return v | views::filter(is_even);
}
Grab from old week 4