When should I use raw pointers over smart pointers?

C++PointersBoostSmart Pointers

C++ Problem Overview


After reading this answer, it looks like it is a best practice to use smart pointers as much as possible, and to reduce the usage of "normal"/raw pointers to minimum.

Is that true?

C++ Solutions


Solution 1 - C++

No, it's not true. If a function needs a pointer and has nothing to do with ownership, then I strongly believe that a regular pointer should be passed for the following reasons:

  • No ownership, therefore you don't know what kind of a smart pointer to pass
  • If you pass a specific pointer, like shared_ptr, then you won't be able to pass, say, scoped_ptr

The rule would be this - if you know that an entity must take a certain kind of ownership of the object, always use smart pointers - the one that gives you the kind of ownership you need. If there is no notion of ownership, never use smart pointers.

Example1:

void PrintObject(shared_ptr<const Object> po) //bad
{
    if(po)
      po->Print();
    else
      log_error();
}

void PrintObject(const Object* po) //good
{
    if(po)
      po->Print();
    else
      log_error();
}

Example2:

Object* createObject() //bad
{
    return new Object;
}

some_smart_ptr<Object> createObject() //good
{
   return some_smart_ptr<Object>(new Object);
}

Solution 2 - C++

Using smart pointers to manage ownership is the right thing to do. Conversely, using raw pointers wherever ownership is not an issue is not wrong.

Here are some perfectly legitimate use of raw pointers (remember, it is always assumed they are non-owning):

where they compete with references

  • argument passing; but references can't be null, so are preferable
  • as class members to denote association rather than composition; usually preferable to references because the semantics of assignment are more straightforward and in addition an invariant set up by the constructors can ensure that they are not 0 for the lifetime of the object
  • as a handle to a (possibly polymorphic) object owned somewhere else; references can't be null so again they are preferable
  • std::bind uses a convention where arguments that are passed are copied into the resulting functor; however std::bind(&T::some_member, this, ...) only makes a copy of the pointer whereas std::bind(&T::some_member, *this, ...) copies the object; std::bind(&T::some_member, std::ref(*this), ...) is an alternative

where they do not compete with references

  • as iterators!
  • argument passing of optional parameters; here they compete with boost::optional<T&>
  • as a handle to a (possibly polymorphic) object owned somewhere else, when they can't be declared at the site of initialization; again, competing with boost::optional<T&>

As a reminder, it's almost always wrong to write a function (that is not a constructor, or a function member that e.g. takes ownership) that accepts a smart pointer unless it in turn pass it to a constructor (e.g. it's correct for std::async because semantically it's close to being a call to the std::thread constructor). If it's synchronous, no need for the smart pointer.


To recap, here's a snippet that demonstrates several of the above uses. We're writing and using a class that applies a functor to every element of an std::vector<int> while writing some output.

class apply_and_log {
public:
    // C++03 exception: it's acceptable to pass by pointer to const
    // to avoid apply_and_log(std::cout, std::vector<int>())
    // notice that our pointer would be left dangling after call to constructor
    // this still adds a requirement on the caller that v != 0 or that we throw on 0
    apply_and_log(std::ostream& os, std::vector<int> const* v)
        : log(&os)
        , data(v)
    {}

    // C++0x alternative
    // also usable for C++03 with requirement on v
    apply_and_log(std::ostream& os, std::vector<int> const& v)
        : log(&os)
        , data(&v)
    {}
    // now apply_and_log(std::cout, std::vector<int> {}) is invalid in C++0x
    // && is also acceptable instead of const&&
    apply_and_log(std::ostream& os, std::vector<int> const&&) = delete;

    // Notice that without effort copy (also move), assignment and destruction
    // are correct.
    // Class invariants: member pointers are never 0.
    // Requirements on construction: the passed stream and vector must outlive *this

    typedef std::function<void(std::vector<int> const&)> callback_type;

    // optional callback
    // alternative: boost::optional<callback_type&>
    void
    do_work(callback_type* callback)
    {
        // for convenience
        auto& v = *data;

        // using raw pointers as iterators
        int* begin = &v[0];
        int* end = begin + v.size();
        // ...

        if(callback) {
            callback(v);
        }
    }

private:
    // association: we use a pointer
    // notice that the type is polymorphic and non-copyable,
    // so composition is not a reasonable option
    std::ostream* log;
    
    // association: we use a pointer to const
    // contrived example for the constructors
    std::vector<int> const* data;
};

Solution 3 - C++

The use of smart pointers is always recommended because they clearly document the ownership.

What we really miss, however, is a "blank" smart pointer, one that does not imply any notion of ownership.

template <typename T>
class ptr // thanks to Martinho for the name suggestion :)
{
public:
  ptr(T* p): _p(p) {}
  template <typename U> ptr(U* p): _p(p) {}
  template <typename SP> ptr(SP const& sp): _p(sp.get()) {}

  T& operator*() const { assert(_p); return *_p; }
  T* operator->() const { assert(_p); return _p; }

private:
  T* _p;
}; // class ptr<T>

This is, indeed, the simplest version of any smart pointer that may exist: a type that documents that it does not own the resource it points too.

Solution 4 - C++

One instance where reference counting (used by shared_ptr in particular) will break down is when you create a cycle out of the pointers (e.g. A points to B, B points to A, or A->B->C->A, or etc). In that case, none of the objects will ever be automatically freed, because they are all keeping each other's reference counts greater than zero.

For that reason, whenever I am creating objects that have a parent-child relationship (e.g. a tree of objects), I will use shared_ptrs in the parent objects to hold their child objects, but if the child objects need a pointer back to their parent, I will use a plain C/C++ pointer for that.

Solution 5 - C++

Few cases, where you may want to use pointers:

  • Function pointers (obviously no smart pointer)
  • Defining your own smart pointer or container
  • Dealing with low level programming, where raw pointers are crucial
  • Decaying from raw arrays

Solution 6 - C++

I think a little bit more thorough answer was given here: https://stackoverflow.com/questions/8706192/which-kind-of-pointer-do-i-use-when

Excerpted from that link: "Use dumb pointers (raw pointers) or references for non-owning references to resources and when you know that the resource will outlive the referencing object / scope." (bold preserved from the original)

The problem is that if you're writing code for general use it's not always easy to be absolutely certain the object will outlive the raw pointer. Consider this example:

struct employee_t {
    employee_t(const std::string& first_name, const std::string& last_name) : m_first_name(first_name), m_last_name(last_name) {}
    std::string m_first_name;
    std::string m_last_name;
};

void replace_current_employees_with(const employee_t* p_new_employee, std::list<employee_t>& employee_list) {
    employee_list.clear();
    employee_list.push_back(*p_new_employee);
}

void main(int argc, char* argv[]) {
    std::list<employee_t> current_employee_list;
    current_employee_list.push_back(employee_t("John", "Smith"));
    current_employee_list.push_back(employee_t("Julie", "Jones"));
    employee_t* p_person_who_convinces_boss_to_rehire_him = &(current_employee_list.front());

    replace_current_employees_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list);
}

Much to its surprise, the replace_current_employees_with() function can inadvertently cause one of its parameters to be deallocated before it's finished using it.

So even though it might at first seem like the replace_current_employees_with() function doesn't need ownership of it's parameters, it needs some kind of defense against the possiblity of its parameters being insidiously deallocated before it's finished using them. The simplest solution is to actually take (temporary shared) ownership of the parameter(s), presumably through a shared_ptr.

But if you really don't want to take ownership, there is now a safe option - and this is the shameless plug portion of the answer - "registered pointers". "registered pointers" are smart pointers that behave like raw pointers, except that they are (automatically) set to null_ptr when the target object is destroyed, and by default, will throw an exception if you try to access an object that has already been deleted.

Also note that registered pointers can be "disabled" (automatically replaced with their raw pointer counterpart) with a compile-time directive, allowing them to be used (and incur overhead) in debug/test/beta modes only. So you should really have to resort actual raw pointers quite rarely.

Solution 7 - C++

I believe smart pointer should be used as much as possible, even in the situation where raw pointer is enough. unique_ptr can help manage the resource life cycle while still staying small and fast. Don't look back!

Solution 8 - C++

It is true. I can not see the benefits of raw pointers over smart pointers, especially in a complex project.

For tempory and lightweight usage, raw pointers are fine though.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlon GubkinView Question on Stackoverflow
Solution 1 - C++Armen TsirunyanView Answer on Stackoverflow
Solution 2 - C++Luc DantonView Answer on Stackoverflow
Solution 3 - C++Matthieu M.View Answer on Stackoverflow
Solution 4 - C++Jeremy FriesnerView Answer on Stackoverflow
Solution 5 - C++iammilindView Answer on Stackoverflow
Solution 6 - C++NoahView Answer on Stackoverflow
Solution 7 - C++frankView Answer on Stackoverflow
Solution 8 - C++TrombeView Answer on Stackoverflow