It would be nice if C++ had better support for smart pointers

This is more of a gripe than a question, but I've been getting into using smart pointers in C++. While they provide a lot of advantages over raw pointers, they do make debugging a lot more difficult.

The errors I get when linking when I've made a mistake with smart pointers are verbose and have little to do with the actual problem. When debugging, if I step into a method on a smart pointer object, I have to descend through about 6 layers of API calls before I get to the method I'm stepping into. std::enable_shared_from_this seems like a hack and requires you to make sure you already have a pointer to your object before you can use it. Any you're constantly writing std::shared_ptr<typename> and std::make_shared<typename>() everywhere.

It would just be really nice if in the future C++ could make smart pointers part of the language and not just in the std library.
Last edited on
constantly writing std::shared_ptr<typename> and std::make_shared<typename>() everywhere


Constantly? Are you constantly sharing data between threads?
Last edited on
No, you just need to write it out whenever you want to use a pointer. auto helps a bit, but you still need to write it in many places. A lot more tedious and error prone than just using *.
One could write a book in reply, but they've been written already.

enable_shared_from_this isn't something one uses frequently, but the design has been fairly well considered. If you've ever tried to implement something like it yourself you'd end up either creating serious problems under certain circumstances, or you'd arrive at similar conclusions as the library has implemented. There's an extremely good reason one must ensure a pointer already holds the object sharing "from this". I'll leave that to your reading as you progress along.

One point mbozzi is referencing here is that shared_ptr is most applicable where threaded development is concerned, though it has purpose in other contexts. There is a cost to reference counted containment that should be considered (and becomes habit after some time). This means that we (elders of decades in practice) realize that the early use of shared_ptr and similar smart pointers from other libraries, back in the late 90's and early 21st century, was way too widespread and poorly considered.

That said, an early technique to avoid the additional typing is this concept:

typedef std::shared_ptr< T > T_SPtr;

A more modern "using" version may better fit current standards, but the point is that one doesn't have to repeat the longer version of the type. This continues on to other versions of this concept, like:

typedef std::vector< T_SPtr > T_Vptrs;

I'm not bothering to get into the significant details or write code. The names of the types I'm showing have no thought behind them. This second version merely shows that a type can be created representing a vector of shared pointers, and then if one needed a shared pointer to that container:

typedef std::shared_ptr< T_Vptrs > T_Vecsptr;

Listed once, this goes a long way to shortening what you type.

Naked "std::make_shared< T >()" may not be all that wise, either. A function is likely better:


std::shared_ptr< T > NewT() { return std::make_shared< T >() };


This is most likely a member function which "knows" how to make a T for you, but it can certainly be a non-member. The point is that making a T may not always be trivial, or remain the same as you develop.

Any time you find yourself repeating the same thing, it's a hint that that thing should probably be in a function or part of a class. That class or function is calling to you as you write, trying to show you it's there through the repetition.

Other smart pointers are worth at least as much study. unique_ptr requires a look, and some adjustment in thinking. scoped_ptr deserves as much attention.

Not using any pointer also deserves some reading. Where we once used pointers because of their efficiency relative to returning copies, the compiler takes over an elides copies to the point that the paradigm of using pointers changed. Indeed, returning tuples and other complex types used to cost a lot, but they're almost entirely free of performance impact in the last few years.



No, you just need to write it out whenever you want to use a pointer. auto helps a bit, but you still need to write it in many places. A lot more tedious and error prone than just using *.


My point is that using shared_ptr constantly (or even frequently) is probably a bad sign.

Use naked pointers frequently, unique_ptr occasionally, shared_ptr rarely, and weak_ptr only if you must.

In particular shared_ptr should not be used blindly as a replacement for naked pointers. This practice misses the purpose of shared_ptr entirely and will result in programs that are less performant and less correct.
Last edited on
Well, I've been getting a lot of conflicting advice about this. Quite of few of the posts I've read and received tell me that raw pointers should no longer be used because they have no concept of ownership, but then I'm told using smart pointers indicates poor program design. I can find no clear guide on the proper way to use smart pointers in my code.

The situation I'm most concerned with is that of a graph that contains nodes that other parts of the program should be able to see. For example, I may want to pass my graph to a GUI component or some database function. These parts of the program will need to take nodes from the graph so they can operate on them. I can't use unique_ptr since my graph still needs to own them and is not going to give them up just so the GUI can check their properties. The nodes also need to have pointers to each other to indicate connections. shared_ptr seems the only safe way to do this. I could use raw pointers instead, but then you have the old problem of never being sure if the pointer has already been deleted.

Using typedef seems like a way to clean up some code. I'll try using that.
Last edited on
Your last post suggests you have a confusion between smart pointers and shared_ptr.

While shared_ptr is a smart pointer, it is not the only kind of smart pointer, and is intended for certain situations.

Not all smart pointers are shared pointers.

Shared pointers a referenced counted containment (often said as ownership). Multiple "references" to the underlying memory can be shared among threads or various objects, and they all "own" the single object through the reference counting paradigm. That is to say, when the last "holder" of a shared_ptr to the same underlying object "let's go", the object is deleted.

This is not the paradigm of unique_ptr. unique_ptr will not share containment. There are provisions to allow you to move ownership, but only one unique_ptr will own an object at a time.

scoped_ptr is more intended for use on the stack, and does not share containment.

When kitfox says "Use naked pointers frequently..." I don't think (and hope that it doesn't) mean what you might think of it.

It is true that naked pointers are known source of bugs, but they are fast. For most situations they should be avoided where containment is required. They are used more for fast sequencing, passing parameters to older libraries or operating system calls (that require them), but not as a containment strategy.

Containment is often handled by...containers...like vector, list, map, etc. For a container of 1, there are smart pointers, and they do similar things.

Naked pointers offer few advantages, but they are required for legacy interfacing.

For all development work involving "new" objects (created on the heap), a containment strategy should be considered, and that should be either smart pointers (not always shared_ptr), or containers, or something known to be reliable (and automatic).

You need to read Stroustrup on these points. Works by Josuttis or Sutter are similarly important.


Quite of few of the posts I've read and received tell me that raw pointers should no longer be used because they have no concept of ownership

No. In a modern C++ program, raw pointers explicitly disclaim ownership; they do not own. This is perfectly compatible with any useful model of ownership, which must include "no ownership".

Rather than beating a long-since dead horse, start with the C++ Core Guidelines:
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f7-for-general-use-take-t-or-t-arguments-rather-than-smart-pointers
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-summary-smartptrs
Then get a textbook by a reputable author:
https://www.stroustrup.com/4th.html

Niccolo's got the right idea.

Niccolo wrote:
When [mbozzi] says "Use naked pointers frequently..." I don't think (and hope that it doesn't) mean what you might think of it.

I mean that it should be far more common to use pointers which do not own the pointed-to object vs. pointers which do.
Last edited on
How would you decide to use pointers (or containers) in this situation?

You have a class called Library that can contain 0 or more Document objects. Library provides methods like createNewDocument(), removeDocument() and getDocument(int index) to manage the Documents. You also have a GUI interface object called DocumentPanel which is used to display the document. The DocumentPanel has a setDocument() method so it can be assigned the Document it is rendering.

What would be the best way to design a data structure to handle this situation? Having Library keep std::vector<std::shared_ptr<Document>> makes the most sense to me. Either that or use raw pointers and be very careful to make sure that whenever you call removeDocument() that any DocumentPanel that is using that document has already been deleted.

What would be the best way to design a data structure to handle this situation?

We might be able to make concrete suggestions if you answer the following:
1. You said you had a graph. I'll assume from context that documents hold pointers to other documents. In any event, is your graph acyclic? Is it directed?
2. Can you post the class definition of Library and Document (with or without an implementation)?
Last edited on
The second example I gave is different from the first. While I do have a graph I'm working with, I think the Document example is a bit more straight forward.

Below is pseudocode for a situation I encounter a lot. Basically I have one class that creates and indexes a set of objects, and another class that uses those objects - like checking a book out of a library. Here my DocumentPanel is managed by the GUI and needs to keep track of a Document that was created by the Library. (The GUI API code here is for no specific GUI, but captures the essence of the things I typically do)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <memory>
#include <vector>

class Document
{
    std::string _text;

public:
    const std::string& text() { return _text; }
    void setText(const std::string& text)
    {
        _text = text;
    }
};

class Library
{
    std::vector<std::shared_ptr<Document>> _documents;
    
public:
    std::shared_ptr<Document> createDocument()
    {
        auto doc = std::make_shared<Document>();
        _documents.push_back(doc);
        return doc;
    }

    void removeDocument(int index)
    {
        _documents.erase(_documents.begin() + index);
    }
    
    std::shared_ptr<Document> getDocument(int index)
    {
        return _documents.at(index);
    }    
}

class DocumentPanel : public SomeGUIPanel
{
    std::shared_ptr<Document> _doc;
public:

    void setDocument(std::shared_ptr<Document> doc)
    {
        _doc = doc;
    }
    
    void drawPanel(GUIRenderContext* ctx) override
    {
        if (_doc)
            ctx->drawString(_doc->text(), 0, 50);
    }
}

int main()
{
    static Library lib;
    auto doc = lib.createDocument();
    doc->setText("foobar");
    
    //Using legacy API system to create GUI
    MyGUIApp* app = createMainApp();
    
    DocumentPanel* panel = new DocumentPanel();
    panel->setDocument(doc);
    app->getMainWindow()->addTab("Document 1", panel);

    app->run();
}
Last edited on
As mbozzi points out it can be difficult to speak only in generalities or vague notions of a design, but I notice something implied by DocumentPanel.

In modern GUI, it is common, as an advanced technique, to provide threads to such a display object, so that it can perform work on painting the document while other windows do the same, at the same time.

As you also probably have an executive, the library portion that creates documents as an example, it may well work in its own thread, or the main UI thread. This implies threads which must share ownership of the document, and that does suggest a shared pointer to the document object, much as you illustrate.

These decisions come with experience in the use of these libraries, and the books on the list mbozzi links to are classics of high order, some that walk you through the logistics of the questions you're asking. The answers aren't simple, and there is overlap.

Stroustrup's own books are quite good, but his list includes other luminary authors in the same caliber. Walk through them. They are based on decades of knowledge and experience, so read them with the assumption that the subjects have been well thought through.

mbozzi included weak_ptr, which you've not mentioned, which applies directly to his inquiry about an acyclic graph. There is a special problem, perhaps a weakness itself, if a chain of smart pointers end up pointing to itself, creating a loop. Such a construct would never actually be freed without a careful and explicit loop to deal with it (and that is not logically reliable). Instead, using weak_ptrs, itself a slightly cumbersome paradigm to use, solves this problem.

The weak_ptr represents a design pattern specifically for this problem, and it is quite reliable. It is also a bit slower, because one creates a shared_ptr from the weak_ptr in order to use the object, making this an extra bit of work. Still, it is better than vast memory leaks that are otherwise not obvious or easy to debug. There are generally no crashes (except for memory exhausting, if that can even happen on a particularly well endowed machine).

Your closing point should make you think. It is actually a solution, though not ideal, typically. The very containers in the standard library were created to do exactly that, from that same viewpoint. Indeed, there are situations where we are required to manipulate raw memory (usually in large blocks) in a library in the old fashioned C style (and sometimes because the operating system provides this to us in a C style).

The key is to wrap that in the library, and debug it to perfection - as the containers in the standard library have done (with historical exceptions in certain libraries and compilers).

That will not come to your work until well into your study. A library writer is a programmer's programmer. They must be very good at the engineering.

Also, while vector is well done and works as you'd expect, there is an overlooked container with similar features that may be better suited than many give credit. The deque container. It can operate much like an array, but is more efficiently expanded and contracted over time. It is more complex, and isn't as fast, but close enough to be a replacement under some situations, and your example may well be one of them.

Topic archived. No new replies allowed.