trainings/AdvancedCppV2/Presentation/smart_pointers_unique_ptr.md

9.7 KiB
Raw Blame History

std::unique_ptr<>


std::unique_ptr<>

unique pointers

std::unique_ptr<> usage

  • Old style approach vs modern approach
#include <iostream> // old-style approach


struct Msg {
    int getValue() { return 42; }
};

Msg* createMsg() {
    return new Msg{};
}

int main() {
    auto msg = createMsg();

    std::cout << msg->getValue();
    delete msg;
}
#include <memory> // modern approach
#include <iostream>

struct Msg {
    int getValue() { return 42; }
};

std::unique_ptr<Msg> createMsg() {
    return std::make_unique<Msg>();
}

int main() {
    // unique ownership
    auto msg = createMsg();

    std::cout << msg->getValue();
}


std::unique_ptr<> usage

  • Copying is not allowed
  • Moving is allowed
std::unique_ptr<MyData> source(void);
void sink(std::unique_ptr<MyData> ptr);

void simpleUsage() {
    source();
    sink(source());
    auto ptr = source();
    // sink(ptr);       // compilation error
    sink(std::move(ptr));
    auto p1 = source();
    // auto p2 = p1;    // compilation error
    auto p2 = std::move(p1);
    // p1 = p2;         // compilation error
    p1 = std::move(p2);
}
std::unique_ptr<MyData> source(void);
void sink(std::unique_ptr<MyData> ptr);

void collections() {
    std::vector<std::unique_ptr<MyData>> v;
    v.push_back(source());

    auto tmp = source();
    // v.push_back(tmp); // compilation error
    v.push_back(std::move(tmp));

    // sink(v[0]);       // compilation error
    sink(std::move(v[0]));

}

std::unique_ptr<> problem with containers

What is wrong with this part of code?
std::unique_ptr<MyData> source(void);
void sink(std::unique_ptr<MyData> ptr);

void collections() {
    std::vector<std::unique_ptr<MyData>> v;
    v.push_back(source());

    auto tmp = source();
    v.push_back(std::move(tmp));

    sink(std::move(v[0]));
    std::cout << *(v[0]) << '\n';
}

std::unique_ptr<> cooperation with raw pointers

#include <memory>

void legacyInterface(int*) {}
void deleteResource(int* p) { delete p; }
void referenceInterface(int&) {}

int main() {
    auto ptr = std::make_unique<int>(5);
    legacyInterface(ptr.get());
    deleteResource(ptr.release());
    ptr.reset(new int{10});
    referenceInterface(*ptr);
    ptr.reset(); // ptr is a nullptr
    return 0;
}
  • get() returns a raw pointer without releasing the ownership
  • release() returns a raw pointer and release the ownership
  • reset() replaces the manager object
  • operator*() dereferences pointer to the managed object

std::make_unique()

#include <memory>

struct Msg {
    Msg(int i) : value(i) {}
    int value;
};

int main() {
    auto ptr1 = std::unique_ptr<Msg>(new Msg{5});
    auto ptr2 = std::make_unique<Msg>(5);   // equivalent to above
    return 0;
}

std::make_unique() is a factory function that produce unique_ptrs

  • added in C++14 for symmetrical operations on unique and shared pointers
  • avoids bare new expression

std::unique_ptr<T[]>

struct MyData {};

void processPointer(MyData* md) {}
void processElement(MyData md) {}

using Array = std::unique_ptr<MyData[]>;

void use(void)
{
    Array tab{new MyData[42]};
    processPointer(tab.get());  
    processElement(tab[13]);
}
  • During destruction
    • std::unique_ptr<T> calls delete
    • std::unique_ptr<T[]> calls delete[]
  • std::unique_ptr<T[]> has additional operator[] for accessing array element
  • Usually std::vector<T> is a better choice

Exercise: Resource

  1. Compile and run Resource application
  2. Check memory leaks under valgrind
  3. Fix memory leaks with a proper usage of delete operator
  4. Refactor the solution to use std::unique_ptr<>
  5. Use std::make_unique()

Exercise: Converter

  1. Compile and run Converter application and check memory leaks under valgrind
  2. Fix code using std::unique_ptr and std::make_unique
  3. Find other issues and fix them (use good practise etc...)

Why virtual D'tor is so important (1)?

class Resource {
public:
    explicit Resource(const std::string& str): str_(str) {}

    const std::string& str() const {
        return str_;
    }

private:
    std::string str_;
};

class Converter {
public:
    Converter() {
         std::cout << "C'tor converter\n";
    }

    virtual ~Converter() {
         std::cout << "D'tor converter\n";
    }

    virtual void Convert(const std::unique_ptr<Resource>& resource) const = 0;
};

class CurlyBracketConverter : public Converter {
public:
    CurlyBracketConverter() {
         std::cout << "C'tor CurlyBracketConverter\n";
    }

    ~CurlyBracketConverter() override {
        std::cout << "D'tor CurlyBracketConverter\n";
    }

    void Convert(const std::unique_ptr<Resource>& resource) const override {
        std::cout << "{" << resource->str() << "}\n";
    }
};

class SquareBracketConverter : public Converter {
public:
    SquareBracketConverter() {
         std::cout << "C'tor SquareBracketConverter\n";
    }

    ~SquareBracketConverter() override {
        std::cout << "D'tor SquareBracketConverter\n";
    }

    virtual void Convert(const std::unique_ptr<Resource>& resource) const override{
        std::cout << "[" << resource->str() << "]\n";
    }
};

class Printer {
public:
    explicit Printer(std::unique_ptr<Converter> converter): converter_(std::move(converter)) {}

    void Print(const std::unique_ptr<Resource>& resource) const {
        converter_->Convert(resource);
    }

private:
    std::unique_ptr<Converter> converter_;
};

int main() {
    auto resource = std::make_unique<Resource>("Ala has a cat");
    Printer printer(std::make_unique<SquareBracketConverter>());
    Printer printer2(std::make_unique<CurlyBracketConverter>());

    return 0;
}

Why virtual D'tor is so important (2)?

C'tor converter
C'tor SquareBracketConverter
C'tor converter
C'tor CurlyBracketConverter
D'tor converter
D'tor converter

Try to add virtual to your D'tor and check result

C'tor converter
C'tor SquareBracketConverter
C'tor converter
C'tor CurlyBracketConverter
D'tor CurlyBracketConverter
D'tor converter
D'tor SquareBracketConverter
D'tor converter

Custom Deleter

  • When there is a special way to delete object
  • Type of unique_ptr change!
class Foo {
public:
    Foo() {
        std::cout << "Foo C'tor\n";
    }

    void print() const {
        std::cout << "Foo!\n";
    }

private:
    // For some reason, we allow only this function to delete object
    friend void deleteMe(Foo* const foo);

    ~Foo() {
        std::cout << "Foo D'tor\n";
    }
};

void deleteMe(Foo* const foo) {
    std::cout << "Delete object Foo!\n";
    delete foo;
}

int main() {
    // Can't use make unique, need to use unique_ptr C'tor
    // unique_ptr(pointer __p, const deleter_type& __d) noexcept
    std::unique_ptr<Foo, void(*)(Foo* const)> ptr(new Foo, deleteMe);
    ptr->print();

    return 0;
}

Output
Foo C'tor
Foo!
Delete object Foo!
Foo D'tor