std::shared_ptr (Alternative Take: Transition From Raw Pointers)#

This is a task that I gave to students, expanding on std::shared_ptr: refactoring memory management in an existing program by replacing raw pointers with something better.

The (stripped-down) program can be called like so,

  • Using the test/mock implementation of some interface

    $ ./program test
    
  • Using the real implementation of some interface (usually using some real hardware on a Linux system, like GPIO or I2C)

    $ ./program real
    

Original Version: Raw Pointers#

A heap allocation of either TestImplemention or RealImplemention is made, depending on the value of the commandline parameter, and assigned to an Interface (the abstract base class of both) pointer my_obj.

Problem: avoiding memory leaks is hard - the allocation must be freed, so we must take care to delete the object that we allocated.

#include <string>
#include <iostream>


class Interface
{
public:
    virtual ~Interface() = default;
    virtual void doit() const = 0;
};

class TestImplemention : public Interface
{
public:
    ~TestImplemention() override {
        std::cout << "TestImplemention::~TestImplemention()" << std::endl;
    }
    void doit() const override {
        std::cout << "TestImplemention::doit()" << std::endl;
    }
};

class RealImplemention : public Interface
{
public:
    RealImplemention(const std::string& bus, unsigned address) { /*...*/ }
    ~RealImplemention() override {
        std::cout << "RealImplemention::~RealImplemention()" << std::endl;
    }
    void doit() const override {
        std::cout << "RealImplemention::doit()" << std::endl;
    }
};

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cerr << "nix one parameter" << std::endl;
        return 1;
    }

    Interface* my_obj;

    std::string how = argv[1]; // "test" or "real"
    if (how == "test")
        my_obj = new TestImplemention();
    else if (how == "real")
        my_obj = new RealImplemention(/*bus*/"/dev/i2c-1", /*address*/0x42);
    else
        std::cerr << "nix good parameter" << how << std::endl;

    my_obj->doit();                                    // <-- use polymorphic object

    delete my_obj;                                     // <-- what if I forget this?

    return 0;
}

Replacing Raw With Shared Pointers (Clumsy Version)#

Solution: using shared pointers. I the program below we replaced all raw pointer types with their correspondingly typed std::shared_ptr<> template instances. As my_obj goes out of scope (as a consequence of the return statement), the allocation is freed automatically.

Problem: much redundant writing

#include <string>
#include <iostream>
#include <memory>


class Interface
{
public:
    virtual ~Interface() = default;
    virtual void doit() const = 0;
};

class TestImplemention : public Interface
{
public:
    ~TestImplemention() override {
        std::cout << "TestImplemention::~TestImplemention()" << std::endl;
    }
    void doit() const override {
        std::cout << "TestImplemention::doit()" << std::endl;
    }
};

class RealImplemention : public Interface
{
public:
    RealImplemention(const std::string& bus, unsigned address) { /*...*/ }
    ~RealImplemention() override {
        std::cout << "RealImplemention::~RealImplemention()" << std::endl;
    }
    void doit() const override {
        std::cout << "RealImplemention::doit()" << std::endl;
    }
};

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cerr << "nix one parameter" << std::endl;
        return 1;
    }

    std::shared_ptr<Interface> my_obj;                 // <-- ex "Interface*"

    std::string how = argv[1]; // "test" or "real"
    if (how == "test")
        my_obj = std::shared_ptr<TestImplemention>(
            new TestImplemention());                   // <-- this is clumsy
    else if (how == "real")
        my_obj = std::shared_ptr<RealImplemention>(
            new RealImplemention(
                /*bus*/"/dev/i2c-1", /*address*/0x42));// <-- this is clumsy
    else
        std::cerr << "nix good parameter " << how << std::endl;

    my_obj->doit();

    /**/                                               // <-- automatic delete

    return 0;
}

Using std::make_shared<>() (Less Clumsy)#

Reducing redundancy by using the std::make_shared<>() template function.

Note:

  • new is not called directly

  • The constructor is not called directly

  • The parameters of the RealImplemention constructor are forwarded through by std::make_shared<>() (see here for more)

#include <string>
#include <iostream>
#include <memory>


class Interface
{
public:
    virtual ~Interface() = default;
    virtual void doit() const = 0;
};

class TestImplemention : public Interface
{
public:
    ~TestImplemention() override {
        std::cout << "TestImplemention::~TestImplemention()" << std::endl;
    }
    void doit() const override {
        std::cout << "TestImplemention::doit()" << std::endl;
    }
};

class RealImplemention : public Interface
{
public:
    RealImplemention(const std::string& bus, unsigned address) { /*...*/ }
    ~RealImplemention() override {
        std::cout << "RealImplemention::~RealImplemention()" << std::endl;
    }
    void doit() const override {
        std::cout << "RealImplemention::doit()" << std::endl;
    }
};

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cerr << "nix one parameter" << std::endl;
        return 1;
    }

    std::shared_ptr<Interface> my_obj;

    std::string how = argv[1]; // "test" or "real"
    if (how == "test")
        my_obj = std::make_shared<TestImplemention>(); // <-- less clumsy
    else if (how == "real")
        my_obj = std::make_shared<RealImplemention>(
            /*bus*/"/dev/i2c-1", /*address*/0x42);     // <-- less clumsy
    else
        std::cerr << "nix one parameter " << how << std::endl;

    my_obj->doit();

    return 0;
}