trainings/AdvancedCppV2/Presentation/smart_pointers_best_practices.md

7.9 KiB
Raw Blame History

Best practices


Best practices

  • Rule of 0, Rule of 5
  • Avoid explicit new
  • Use std::make_shared() / std::make_unique()
  • Avoid copying std::shared_ptr<> when this is not neccessary.
  • Use references instead of pointers (as argument to function)
  • Almost always use unique_ptr
  • Use shared_ptr ONLY when you need to share ownership of object.

Rule of 0, Rule of 5

Rule of 5

  • If you need to implement one of those functions:
    • destructor
    • copy constructor
    • copy assignment operator
    • move constructor
    • move assignment operator
  • It probably means that you should implement them all, because you have manual resources management.

Rule of 0

  • If you use RAII wrappers on resources, you dont need to implement any of Rule of 5 functions.

Avoid explicit new

  • Smart pointers eliminate the need to use delete explicitly
  • To be symmetrical, do not use new as well
  • Allocate using:
    • std::make_unique()
    • std::make_shared()
  • use new only when you need to create ptr with custom deleter

Use std::make_shared() / std::make_unique()

  • What is a problem here?
struct MyData { int value; };
using Ptr = std::shared_ptr<MyData>;
void sink(Ptr oldData, Ptr newData);

void use(void) {
    sink(Ptr{new MyData{41}}, Ptr{new MyData{42}});
}
  • Hint: this version is not problematic
struct MyData { int value; };
using Ptr = std::shared_ptr<MyData>;
void sink(Ptr oldData, Ptr newData);

void use(void) {
    Ptr oldData{new MyData{41}};
    Ptr newData{new MyData{42}};
    sink(std::move(oldData), std::move(newData));
}

Allocation deconstructed

auto p = new MyData(10); means:

  • allocate sizeof(MyData) bytes
  • run MyData constructor
  • assign address of allocated memory to p

Order of evaluation of any part of any expression, including order of evaluation of function arguments is unspecified. The compiler can evaluate operands and other subexpressions in any order, and may choose another order when the same expression is evaluated again. There is no concept of left-to-right or right-to-left evaluation in C++. This is not a problem since C++17 due to the changes in the evaluation order of function arguments. Specifically, each argument to a function is required to fully execute before evaluation of other arguments.


Unspecified order of evaluation

  • How about two such operations (before C++17)?
first operation (A) second operation (B)
(1) allocate sizeof(MyData) bytes (1) allocate sizeof(MyData) bytes
(2) run MyData constructor (2) run MyData constructor
(3) assign address of allocated memory to p (3) assign address of allocated memory to p
  • Unspecified order of evaluation means that order can be for example:
    • A1, A2, B1, B2, A3, B3
  • What if B2 throws an exception?

Use std::make_shared() / std::make_unique()

  • std::make_shared() / std::make_unique() resolves this problem
struct MyData{ int value; };
using Ptr = std::shared_ptr<MyData>;
void sink(Ptr oldData, Ptr newData);

void use() {
    sink(std::make_shared<MyData>(41), std::make_shared<MyData>(42));
}
  • Fixes previous bug
  • Does not repeat a constructed type
  • Does not use explicit new
  • Optimizes memory usage (only for std::make_shared())

Copying std::shared_ptr<>

void foo(std::shared_ptr<MyData> p);

void bar(std::shared_ptr<MyData> p) {
    foo(p);
}
  • requires counters incrementing / decrementing
  • atomics / locks are not free
  • will call destructors
Can be better?

Copying std::shared_ptr<>

void foo(const std::shared_ptr<MyData> & p);

void bar(const std::shared_ptr<MyData> & p) {
    foo(p);
}
  • as fast as pointer passing
  • no extra operations
  • not safe in multithreaded applications

Use references instead of pointers

  • What is the difference between a pointer and a reference?
    • reference cannot be empty
    • reference, once assigned cannot point to anything else
  • Priorities of usage (if possible):
    • (const) T&
    • std::unique_ptr<T>
    • std::shared_ptr<T>
    • T*

Exercise: List

Take a look at List.cpp file, where simple (and buggy) single-linked list is implemented.

  • pushFront method adds a new Node at the begining of the list.
  • findByValue method iterates over the list and returns the first Node with matching value or nullptr.
  1. Compile and run List application
  2. Fix memory leaks without introducing smart pointers
  3. Fix memory leaks with smart pointers. What kind of pointers needs to be applied and why?
  4. Add function to add a node at the end of the list (try to do this with time complexity O(1))
  5. Add function to delete node with provided value.