I wrote a pointer (wrapper) and I liked it

Don't use (raw) pointers

I learned C with Oualline's excellent book: Practical C Programming ([2]), without any formal Computer Science education, it played a significant role into my computational thinking development. For years it was my first and only reference for C, even after having access to Kernighan and Ritchie's C book ([3]). The C++ language came into my radar well into my undergraduate studies, I think I adopted it at first just because of my academic community at the time but I remember thinking that it was C with some tweaks. In particular I was happy that I could use pointers but I was happier because of the new and delete constructs.
In general when you request a new memory, you need to initialize it before you do any real work. Having those two steps merged into one instruction (new) was a step forward. Something equivalent happens when you release a chunk of memory, sometimes, before you do, you clean it up by calling a function to do it. This pattern is captured by the delete instruction. Bottom line, I was happy with those two constructs and remained happy for many years. And during that time, I couldn't understand the complains about pointers, for me pointers are simple creatures. On the other hand, for example, I struggled to understand the different semantics of the C++ assignment operator (=) and how they related to the different types of constructors for objects.
The mantra inside the C++ (modern) community is: don't use raw/naked pointers. The bottom line argument is that raw/naked pointers (from now on just pointers) are error-prone. In the C++ Core Guidelines ([1]) pointers make the first appearance early on at the end of Philosophy.3 (P.3).
At P.6 the first complete pointer related example is given:

// separately compiled, possibly dynamically loaded extern void f(int* p); void g(int n) { // bad: the number of elements is not passed to f() f(new int[n]); }

This is an interesting example that I want to look at a bit closer. Pointers and arrays go hand in hand in C. The chapter about pointers in [3] is called: Pointers and Arrays. But a pointer is just a variable whose value is a memory address. The semantics of a pointer, combined with a linear vision of memory, allows to consider memory addresses that come before and after a pointer; concepts suitable to express the semantics of arrays. The model of navigable memory, makes pointer arithmetic possible. We can generalize this vision a little: a pointer can be considered as a lens under which all the system's memory can be interpreted. The actual kind of lens is the type of the pointer.
In the example, there is nothing in f's signature to indicate it expects an array, or that it will access other addresses besides the one stored in p. Since g does not keep track of the pointer sent as an argument to f, f (or any other function reachable from it) is responsible to release the memory pointed by p. This poses an issue for f, because it would not know which release function to use: delete or delete []. This issue can be addressed with proper communication in the documentation of f. However, the underlying problem is that both allocators, new T and new T[...] use the same type T *, to represent two different concepts. That is, the type does not carry enough information, leaving the programmer with no (core) language construct to mitigate this, but rely on engineering best practices. On the other hand, the usual malloc, free functions of the C standard library, handle this case automatically: each pointer has an associated memory size so, when the pointer is freed the whole memory chunk is released.

Dangerously simple

Pointers semantics is as simple as a light switch's in a dark room. If you turn in on (new, malloc), you must turn it off (delete,free). And you can only be in the room if the light is on. That's it.
Yet, this simple and elegant construct is responsible for a plethora of bugs, all boiling down to what is known as pointer dereference, that is, reading the contents of the memory the pointer points to and memory leak, memory is requested but is never released.

Both languages, C and C++, use a no questions asked model to dereference pointers: if you have a pointer variable T *p; then you can read the contents of the memory pointed at by p with the instruction: *p.
There is no mechanism in either language to check for a pointer's validity nor lifetime. At the simplest, a pointer p is valid if and only if, it points to a previously allocated memory chunk. And it is alive as long as a pointer is alive. But these elemental properties implies bookkeeping which does not go well when you want to create fast, compact programs.
It is better to leave the tracking of live pointers to program design. But careful design takes time and time is in short supply in most software development endeavors.

The dichotomy is laid out: we need the benefit of pointers (avoid memory copies) without the required time constraints.
The C++ technical solution is collectively called smart pointers. The main idea of smart pointers is not to simplify the semantics of pointers, they can't really be simplified, but to express different uses into different types. Smart pointers are implemented using templates, so they are not exactly different core types, but they are definitely different types. Smart pointers comprehend three different use-cases: (1) unique_ptr<T> the pointed memory lives for as long as the pointer itself lives. (2) shared_ptr<T> the pointed memory accepts different pointers and live until the last pointer is released, (3) weak_ptr<T> safe pointer dereference.

One could argue that dividing one type into three would increase design time complexity. However, the smart pointers types reflect intent which saves time when the role of the pointer must be recalled (I have asked myself am I supposed to free this now?). My personal favorites are weak and shared pointers.
An unique_ptr<T> pointer is responsible to destroy the object it points to ([5]). When an unique_ptr<T> is created, the pointer receives a raw pointer as an argument and when the unique_ptr<T> is destroyed, the pointer is released. This life-cycle can be replicated with a stack variable.

Avoiding unique_ptr<T>

Perhaps the syntactically cleanest way to mimic unique pointers is by using references. This requires the function at the highest position in the call-chain to be responsible for allocating the memory. Consider the following example:

#include <iostream> void f(int& inc) { inc += 1; } void g(int& mul) { mul *= 2; f(mul); } int main() { int start = 1; g(start); std::cout << "start: " << start << "\n"; }

In it, the address (an alias in C++ reference's lingo) of the variable start is passed to the function g and f. This code behaves exactly as if the start variable were a pointer allocated and destroyed at main. This kind of pattern is suitable when you already hold the data to process and just pass it along some processing functions.
However, it does not work in the reverse direction:

#include <iostream> int& f() { int val = 2; return val; } int& g() { auto v = f(); v += 1; return v; } int main() { auto res = f(); std::cout << "res: " << res << "\n"; return 0; }

In this case the compiler will warn about returning a reference to a local variable (g++ 14.2.1 -Fedora 40-). And if we execute it the program will halt with some kind of segmentation fault. Because we are attempting to use a memory on the stack that is no longer alive. Hence, this patter will not work if we require memory to be allocated further down the call chain and returned.

Since C++11, it is possible to move the value of a variable to a function argument. This operation aims to avoid a full copy and save time and memory. While the overall idea is simple enough the details are not so simple ([4]). It must be used with some care. Let us consider the following structs:

#include <utility> #include <cstddef> #include <iostream> struct simp_pointee { unsigned int val; simp_pointee(unsigned int v):val(v){} }; struct move_struct { struct simp_pointee *ptr; unsigned int val; unsigned int moved_times; move_struct(unsigned int v):ptr(new simp_pointee(v+1)), val(v), moved_times(0){} //Move constructor move_struct(move_struct &&ms):ptr(std::move(ms.ptr)), val(std::move(ms.val)), moved_times(std::move(ms.moved_times)) { moved_times += 1; std::cout << "Move constructor called\n"; std::cout << "\tTimes moved so far: " << moved_times << "\n"; } //Move assignement move_struct& operator=(move_struct &&ms) { ptr = std::move(ms.ptr); val = std::move(ms.val); moved_times = std::move(ms.moved_times); moved_times++; std::cout << "Move assignement\n"; std::cout << "\tTimes moved so far: " << moved_times <<"\n"; return *this; } ~move_struct() { std::cout << "Destructor called \n"; val = -1; if (ptr != nullptr) { std::cout << "\tPtr deleted\n"; delete ptr; ptr = nullptr; } } };

The struct move_struct contains a pointer to an object of type simp_pointee, an integer field and a counter of how many times the move constructor or move assignment operators have been called. Before move constructors, an object constructor created the object from scratch requiring the object to allocate the necessary memory to hold its data. Then a destructor would be required to release and clean up the memory. Constructor-destructor pairs are paired just as new and delete.
Move constructors change this semantics a little bit. The (probably false) intuition behind a move constructor is that resources are transferred between a source and a target. Therefore, if an object is created with this transfer, should its destructor be called? If the answer is yes then, should the destructor of the original object be called? I think not, because the original object does no longer owns any resource. If the destructor of the moved object should not be called then, the destructor of the original must be called, that requires that all movement operations must ensure to restore ownership to the caller. The point is, both options are possible. The following example shows an possibly intuitively correct program that compiles without warnings but performs a double free:

void g(move_struct ms); void f(move_struct ms){ ms.val += 1; std::cout << "------ f called -------\n"; g(std::move(ms)); std::cout << "ms.val: " << ms.val << "\n"; std::cout << "ms.ptr -> val: " << ms.ptr -> val << "\n"; std::cout << "End f\n"; std::cout << "Double free will happen now\n"; } void g(move_struct ms){ ms.ptr -> val += 10; ms.val += 1; std::cout << "------ g called ------\n"; std::cout << "ms.val: " << ms.val << "\n"; std::cout << "ms.ptr -> val: " << ms.ptr -> val << "\n"; std::cout << "Now ms will be destroyed\n"; std::cout << "End g\n"; } int main(){ move_struct ms(1); f(std::move(ms)); std::cout << "main, ms.val: "<< ms.val << "\n"; return 0; }

The intuition behind it is: main creates the variable ms, during this creation a pointer is allocated. Then it transfers the contents to the function f which in turn will transfer the contents to the function g.
Once g finishes its execution, it destroys ms and returns. Upon termination f also destroys its copy of ms which double frees the pointer.

Move constructor called Times moved so far: 1 ------ f called ------- Move constructor called Times moved so far: 2 ------ g called ------ ms.val: 3 ms.ptr -> val: 12 Now ms will be destroyed End g Destructor called Ptr deleted ms.val: 2 ms.ptr -> val: 110185 End f Double free will happen now Destructor called Ptr deleted free(): double free detected in tcache 2

The move constructor seems to be called, and the destructor seems to be called at each function. Therefore, this might suggest the object must be responsible to track its kind of creation so it can handle its destruction properly ([4]). This is also the case when copy constructors are used with pointers that are not deep copied.
The intuition behind the semantics of move, can also lead us to ask the question if the user really needs to control the move besides requesting it. In simple terms it sounds reasonable that once we say f(std::move(ms)) the association behind it: main:msf:ms could be transparent from the user.
In the following example this seems to be the case:

void g(move_struct&& ms); void f(move_struct&& ms){ ms.val += 1; std::cout << "------ f called -------\n"; g(std::move(ms)); std::cout << "ms.val: " << ms.val << "\n"; std::cout << "ms.ptr -> val: " << ms.ptr -> val << "\n"; std::cout << "End f\n"; } void g(move_struct&& ms){ ms.ptr -> val += 10; ms.val += 1; std::cout << "------ g called ------\n"; std::cout << "ms.val: " << ms.val << "\n"; std::cout << "ms.ptr -> val: " << ms.ptr -> val << "\n"; std::cout << "Now ms will be destroyed\n"; std::cout << "End g\n"; } int main(){ move_struct ms(1); f(std::move(ms)); std::cout << "ms.ptr -> val: " << ms.ptr -> val << "\n"; return 0; }

The main change is in the signatures of g and f. The type of their ms arguments is now a rvalue. Which in our setting means an explicit move is expected. The other change is the print at main, if the destructor of the ms object is called as part of the call to function f, then it should trigger some kind of error, e.g. an incorrect value:

------ f called ------- ------ g called ------ ms.val: 3 ms.ptr -> val: 12 Now ms will be destroyed End g ms.val: 3 ms.ptr -> val: 12 End f ms.ptr -> val: 12 Destructor called Ptr deleted

In the run, first we observe that even when the explicit move is requested neither, the move constructor nor the move assignment operator of move_struct is called. That is, the move is transparent to ms. Also, the destructor is called upon main termination, which allows us to get access to a correct ms once f returns. This behavior is consistent if we had used references, however it might be compiler specific. Once a variable is moved its contents should be regarded as meaningless, the variable as a container, is still valid, but the contents should be regarded as invalid. However g++ does not trigger a warning when ms is read at main. So the question arises: is ms alive or dead after f returns control? It seems it is likely to be alive.

While using move semantics to mimic feed-forward pointer like constructs is feasible I think, references are cleaner. We have not solve the feed-backward problem tho. That is, when a function further down the call-chain allocates the memory and needs to return it up the call-chain. It seems that move semantics is used when a function returns a value, so, a function just returning a value might be enough.

Weak and shared pointers

I think weak and shared pointers are much more interesting than unique ones, because after all, I normally use several pointers to point at the same thing. So, the two main natural questions are: (1) Is the memory valid? (2) What are my rights and duties with it?
As a motivating example imagine we are modeling a wireless network connection graph, in which two nodes (transmitters) have an edge if they can communicate, or one hear the other. The nodes move around the space, and so, communications (links) are established and destroyed. Links have properties like intensity, transmission data, etc. On the other hand, nodes rarely cease to exist. One possible way to model a node and a network is as follows:

struct node { unsigned int ident; ... //who can hear me vector<edge *> out_neighbors; //who I can hear vector<edge *> in_neighbors; ... edge *find_edge(unsigned int v){...} void remove_edge(unsigned int v){...} void print_neighbors(){...} }; struct network { ... vector<node *> nodes; ... void add_node(node *n) { nodes.push_back(n); } void add_edge(node *v, node *u) { edge *tmp = new {v->ident,u->ident}; v->out_neighbors.push_back(tmp); u->in_neighbors.push_back(tmp); notify_observers(new_edge,tmp); } void delete_edge(node *v, node *u) { auto e = v -> find_edge(u->ident); if (e != nullptr) delete e; } };

There is a clear mistake at remove_edge and we'll get to that.
The ownership rules seem quite confusing. To create a node, network receives a pointer to a node already created. Therefore it might not own them. Without the code of a destructor or noderemoval this ownership rule can not be defined. edge(s), on the other hand are created by network and destroyed by it. We can conclude that network owns the pointers, however, observers and the nodes can also access the edges.

Race conditions are when two or more threads compete for a result and the outcome depends on the particular timing of the execution. If through the program, only one unit (e.g. network) creates and deletes some type of data (edge) then we can conclude the unit is the owner of that data.
If through the program various units create/delete the same type of data we must ensure that all possible executing paths that involve the units do not introduce illegal memory operations, e.g. double-free or memory leaks. This similarity between incorrectly protected concurrent data and memory allocation and deallocation is addressed by myself and colleagues briefly in [6] (a bit of self-promotion :P)
If through the program a unit only reads some type of data (e.g. node only reads edge), we can conclude the unit is dependent of that data. In this case, we must ensure that every time the unit access the data, it really exists.

Now let's talk about the obvious error. A precondition of node is that it trusts its neighborhood vectors, that is, if an edge is in either (out_neighbors, in_neighbors) the it exists. Unfortunately the code to delete an edge at neighbor, violates this precondition. Just to convince ourselves, we can try it out:

int main() { node *a = new node{0}; node *b = new node{1}; network N; N.add_node(a); N.add_node(b); N.add_edge(a,b); a -> print_neighbors(); b -> print_neighbors(); N.delete_edge(a,b); std::cout << "===Edge deleted===\n"; a -> print_neighbors(); b -> print_neighbors(); return 0; }

And a run:

Neighbors for: 0 Out neighbors: 1 In neighbors: Neighbors for: 1 Out neighbors: In neighbors: 0 ===Edge deleted=== Neighbors for: 0 Out neighbors: 0 In neighbors: Neighbors for: 1 Out neighbors: In neighbors: 30222

The essence of the problem is: The owner of the edge, does not notify all the involved parties that said edge has been removed. In this particular run, the program completed, but the results were incorrect. The solution is:
(1) Notify each node that edge no longer exists. That is, for each node, call remove_edge.
(2) Notify the observers that edge is being removed.
(3) Destroy the edge.
I kept remove_edge with an unsigned int type, just to make it a bit more difficult the role of the pointers. Can we solve this with smart pointers directly? i.e. without adding more code and just by changing the types?

The sensible answer is no. Even if we allow some modification using smart pointers that does not address any of the steps (1) and (2) of the solution, the program will still have a bug. That is, it is a pointers related bug that transcends pointer semantics. Let's try some modifications.
The first, is to change out_neighbors and in_neighbors at node to unique_ptr. We can justify this change arguing that node owns its edges. We also need to modify node::find_edge so it returns an unique_ptr; this, should be hint enough for us that something is wrong. By returning unique_ptr we are relinquishing ownership of our own edge in a consult function. Hence, we need to return a smart pointer that allows consult but does not claim ownership, i.e. weak_ptr.
We have a new issue, to construct a weak_ptr we need a shared_ptr so, we need to convert, at runtime, an unique_ptr into a shared_ptr at a consult function, which makes no sense at all. We can adopt the change from unique_ptr to shared_ptr at the definitions of the vectors out_neighbors and in_neighbors. We can justify this by saying that both ends share the ownership of the edge.
The next problem we face is: how do we delete the edge if the consult function returns a not owning reference?
We can use weak_ptr::lock to acquire temporal ownership of the edge, but this won't work because the most we can do is to release our own temporal ownership. So, we need another version of find_edge to return the shared pointer instead of a weak pointer. So, we have something like this:

struct node { unsigned int ident; std::vector <std::shared_ptr<edge>> out_neighbors; std::vector <std::shared_ptr<edge>> in_neighbors; std::weak_ptr<edge> find_edge(unsigned int v) {...} std::shared_ptr<edge> find_edge_shared(unsigned int v) {...} }; struct network { void delete_edge(node *v, node *u) { std::shared_ptr<edge> tdelete = v -> find_edge_shared(u->ident); tdelete.reset(); } };

If we execute this:

Neighbors for: 0 Out neighbors: 1 In neighbors: Neighbors for: 1 Out neighbors: In neighbors: 0 ===Edge deleted=== Neighbors for: 0 Out neighbors: 1 In neighbors: Neighbors for: 1 Out neighbors: In neighbors: 0

Which is incorrect.
The tdelete.reset() does nothing, but only decrease the reference counter that got incremented by the call to find_edge_shared. The only way to resolve this situation is to remove the edges from the vectors.
The remaining option is to change to weak_ptr. But this immediately conflicts with network::add_edge because the tmp pointer must be a shared_ptr and it will be destroyed upon function termination. Then all edges would be deleted right after being created.

We have constructed a problem, such that it can not be repaired by only using pointer semantics. That is, it is a bug that manifests itself as a pointer error but in fact it is of a different nature. We can think of it as a producer-consumer problem, in which the consumer did not consume a resource correctly.

However, the last idea (changing all the pointers to weak_ptr) pose an appealing option. If we consider removing the trust of the nodes in their neighbors vectors.
We can modify network to have a vector of shared_ptr to edges and keep the vectors of weak_ptrs of neighbors in node. Each time network creates an edge, it stores the shared_ptr in the vector of edges, ensuring its liveness. The function delete_edge just removes the shared_ptr of the edge from the edges vector. Keeping the idea of just delete the edge.
All the weak_ptrs that point to a removed edge become invalid, and each node can check this only when it requires to access the edge!
This approach allows us to lazily remove edges, spreading the complexity of the edge removal operation among several function calls, while keeping functional correctness. It has the penalty that neighbors vectors might be unnecessarily big, but, if the nodes are explored quite often, as in a wireless network simulator, then, this overhead should be amortized.

In the approach previously explained, there is a conceptual mistake. The shared_ptrs in the vector of edges in node are not shared by anyone. This choice was so the weak_ptrs could work. Therefore, it does not reflect that only node owns the edges, which is what is really happening.

Overall I quite like shared and weak pointers, just because the ability to safely peek memory, which is a functionality completely absent in raw/naked pointers. I do not pay too much attention to names, it is quite difficult to succinctly describe functionality in just one word. I prefer to focus on the functionality without regard of its given name. This behavior lead me to design a simple pointer wrapper to reduce the unnecessary (to me) naming noise.

I just want a pointer

I consciously have been keeping away from smart pointers until recently. I wanted to write a file viewer for Anote in C++ using modules. As part of the viewer, I needed a kind of circular double-ended buffer. I decided to use a vector as a container. So far the examples of modules I had seen used things like #include <...> inside the module. So, I figured I can include vector and be done with it. It turned out that I couldn't. At the time of this issue, I didn't know that I could easily compile the module for vector in Linux with g++. So, after testing, I realized I could use raw pointers and the usual allocators and deallocators, so, no big deal, let's write a buffer.

But then I recalled the new mantra about raw pointers, so I figured there is no harm in using smart pointers, but I don't like the complexity of the names, case-uses, and kind of bookkeeping you must perform for them to work. I just wanted to have a pointer to behave like I'm used to in C (and C++) and with some guarantees like safe dereference and bounds checking. I didn't plan to peek memory, so I didn't consider that. But since the original problem is that I want the container for my buffer I wanted my pointer to handle 0 or more elements transparently in a safe way.

So, what do we need? Well it must be a template so we can create pointers of various types of objects. The new pointer class must have keep the raw pointer we are tracking, the number of elements it currently holds, the number of elements it can hold at most and a reference counter. That's it, we don't need much.
The first difference with smart pointers is that I didn't pay attention to the constructors' names. In [5] there is a clear intent to stick to the meaning of names as much as possible, so, when a copy constructor is designed, a copy must be performed. To make my wrapper, instead I asked the questions: in which cases is this constructor called? and then paired it with what is the semantics of a raw pointer in those cases? and finally what should a safe pointer do in that case?
This lead me to a very simple implementation, in which copy constructors just increments a reference counter, and move constructors just move things transparently. As a consequence you require to implement deep copies methods, but that is easy.
For safe access, bounds must be checked, so the operators *,->,[] check bounds and in case of failure throw an exception. I also added safe element retrieval wrapped in an optional value. When the value does not exist an empty optional is returned. When it does exist, an optional with the value is returned.

is it decent?

Smart pointers and the wrapper I wrote can be considered as insurances. And as any insurance, you have to pay a fee, in our case that fee is execution time and memory. If we were to prove that a particular pointer will be valid every time we manipulate it, then there is no need to pay for any insurance, so, it is, for the sake of performance, pointless to use a wrapped pointer. Unlike most insurance companies (:D), we try to keep fees as low as possible.

As part of building a circular buffer, you must use modulo arithmetic or rely on exceptions.
To test the wrapper, I created a wrapper for 10 integers. Transversed it from the index 0 one by one until an exception was triggered. And start over. This transverse was done 10000000 (10M) times. And the experiment was performed 10 times. The fastest speed was: 16426998 ns, the slowest: 17416797 ns.
For comparison, I repeat the same experiment but with a vector big enough to hold 10 elements. The fastest time was: 44642308 ns and the slowest: 46383129 ns Of course a vector has more functionality than just a wrapped-up array. And that is why these times are so different.

Tailored suits should fit you better than ready-to-wear ones and will keep fitting you unless you change just enough. The same principle applies to software. But software usually doesn't change slowly like we do, so is there a point in wasting time in something that next month might change?

References

[1] Stroustrup, B., Sutter, H.(eds.), "C++ Core Guidelines", October 3, 2024. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines (last accessed: May 3, 2025)
[2] Oualline S., "Practical C Programming", 3rd ed, O'Reilly, 1997.
[3] Kernighan, B., Ritchie, D.,"The C programming language", spanish translation, 2nd ed., Prentice-Hall, 1991.
[4] Josuttis, N., "The C++ Standard Library: a tutorial and reference", 2nd ed.,Pearson Education,2012
[5] Stroustrup,B.,"The C++ Programming Language", 4th ed., Addison- Wesley,2013.
[6] Cruz-Carlon, A., et al.,"Patching locking bugs statically with Crayons", TOSEM, Vol.32, no. 3, 2023.

Copyright Alfredo Cruz-Carlon, 2025