33

I have searched and searched for an answer to this but can't find anything I actually "get".

I am very very new to c++ and can't get my head around the use of double, triple pointers etc.. What is the point of them?

Can anyone enlighten me

Mark Green
  • 331
  • 1
  • 3
  • 5
  • 11
    The same as any other pointer – Slava Feb 27 '18 at 18:47
  • You need them for 2d arrays, but this is not a good idea in general. – Arnav Borborah Feb 27 '18 at 18:47
  • 12
    Don't be a [three star programmer](http://wiki.c2.com/?ThreeStarProgrammer). Use data structures to encapsulate the pointers. – NathanOliver Feb 27 '18 at 18:48
  • 21
    _"What is the point of them?"_ Love it – Lightness Races in Orbit Feb 27 '18 at 18:51
  • 1
    Here's one: [Using pointers to remove item from singly-linked list](https://stackoverflow.com/questions/12914917/using-pointers-to-remove-item-from-singly-linked-list) – user4581301 Feb 27 '18 at 19:02
  • If you are in modern C++ and not C, you rarely need to have any pointers because it has nicer alternatives like references, various constructs in the standard library like std::vector, std::list, etc., and when those don't work, std::unique_ptr and std::shared_ptr. For plain old C, I recall there are cases when a double pointer is useful when dealing with linked lists (e.g. having a pointer to the "next" pointer field made some code handling the list simpler). – Dave Feb 27 '18 at 22:19
  • 1
    @Dave: On the contrary, those are more complicated and less clear alternatives to simple and obvious pointers. As for instance what's probably the most common instance of a double pointer (in C): "int main (int argc, char **argv)". – jamesqf Feb 27 '18 at 23:27
  • 1
    It’s unfortunate that every beginner sees `argv` early, because it sets a very bad example. – Davislor Feb 27 '18 at 23:31
  • 1
    One **non**-answer to your question: nobody’s saying that ragged arrays are a good solution when the rows of a matrix contain different amounts of data and you want to save space. If you ever need to do that, make sure you at least consider *compressed sparse row* representation. – Davislor Feb 27 '18 at 23:38
  • So, @MarkGreen, did you get your answer? – Lightness Races in Orbit Mar 04 '18 at 16:27
  • I got a lot of answers and am processing it all c is definitely a complex language with a steep learning curve. – Mark Green Mar 07 '18 at 22:28

11 Answers11

52

Honestly, in well-written C++ you should very rarely see a T** outside of library code. In fact, the more stars you have, the closer you are to winning an award of a certain nature.

That's not to say that a pointer-to-pointer is never called for; you may need to construct a pointer to a pointer for the same reason that you ever need to construct a pointer to any other type of object.

In particular, I might expect to see such a thing inside a data structure or algorithm implementation, when you're shuffling around dynamically allocated nodes, perhaps?

Generally, though, outside of this context, if you need to pass around a reference to a pointer, you'd do just that (i.e. T*&) rather than doubling up on pointers, and even that ought to be fairly rare.

On Stack Overflow you're going to see people doing ghastly things with pointers to arrays of dynamically allocated pointers to data, trying to implement the least efficient "2D vector" they can think of. Please don't be inspired by them.

In summary, your intuition is not without merit.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • 7
    _"In fact, the more stars you have, the closer you are to winning an award of a certain nature."_ - +1ed for that one sentence. – Arnav Borborah Feb 27 '18 at 18:48
  • 6
    @ArnavBorborah: Now if people +1 for each of the other sentences, I'll be doing alright! – Lightness Races in Orbit Feb 27 '18 at 18:51
  • 1
    @LightnessRacesinOrbit You should then end every word with a period to maximize the upvote potential ;) – NathanOliver Feb 27 '18 at 18:57
  • 1
    @NathanOliver: Could do, but I still haven't won the war against comma splices and am not quite ready to start one on period splices :D – Lightness Races in Orbit Feb 27 '18 at 18:58
  • 2
    Anyone who disagrees with the notion of a "three star programmer" should have a look at the [API for (one of?) the most widely deployed SQL engines in the world](http://sqlite.org/capi3ref.html). There is only one function that has a `***` argument, and it's part of a legacy interface. – Daniel Kamil Kozar Feb 27 '18 at 19:14
  • 1
    Some numbers: The project I am currently working on has ~892,000 lines (for simplicity, I counted all lines, including blanks and brace-only lines). 39 occurrences of references to pointers `*&`. About 70 occurrences of pointers to pointers (this is harder to count of course, many false positives on `**`), but almost all of them are in a C compatibility module, with `assert( p != NULL )`, and could therefore be replaced by `*&` in "pure" C++. – Arne Vogel Feb 27 '18 at 19:43
  • @DanielKamilKozar: And even then, we are talking about a **C** API. In C++ there are references and higher-level pointer types which seriously reduce the need for multiple stars. – Matthieu M. Feb 27 '18 at 19:50
  • @MatthieuM. : Of course. I forgot to mention that it's strictly a C API. I wholeheartedly agree with you. – Daniel Kamil Kozar Feb 27 '18 at 19:51
16

An important reason why you should/must know about pointer-to-pointer-... is that you sometimes have to interface with other languages (like C for instance) through some API (for instance the Windows API).

Those APIs often have functions that have an output-parameter that returns a pointer. However those other languages often don't have references or compatible (with C++) references. That's a situation when pointer-to-pointer is needed.

engf-010
  • 3,980
  • 1
  • 14
  • 25
13

It's less used in c++. However, in C, it can be very useful. Say that you have a function that will malloc some random amount of memory and fill the memory with some stuff. It would be a pain to have to call a function to get the size you need to allocate and then call another function that will fill the memory. Instead you can use a double pointer. The double pointer allows the function to set the pointer to the memory location. There are some other things it can be used for but that's the best thing I can think of.

int func(char** mem){
    *mem = malloc(50);
    return 50;
}

int main(){
    char* mem = NULL;
    int size = func(&mem);
    free(mem);
}
user3600107
  • 346
  • 1
  • 2
  • 13
  • 1
    Since the question is tagged with C++ your argument really doesn't answer the question. – NathanOliver Feb 27 '18 at 18:59
  • 3
    @NathanOliver This is almost valid C++ (it's missing a cast). And it's not even useless C++ code - it may be used when interoperating with C. – user253751 Feb 27 '18 at 21:38
  • A real-world example of this: `posix_memalign()`. – Davislor Feb 27 '18 at 23:34
  • I also think that explaining why double pointers are useful in C *is useful* to understanding what they do in C++, even if C++ does offer other ways to do the same things. – user253751 Feb 28 '18 at 02:13
12

I am very very new to c++ and can't get my head around the use of double, triple pointers etc.. What is the point of them?

The trick to understanding pointers in C is simply to go back to the basics, which you were probably never taught. They are:

  • Variables store values of a particular type.
  • Pointers are a kind of value.
  • If x is a variable of type T then &x is a value of type T*.
  • If x evaluates to a value of type T* then *x is a variable of type T. More specifically...
  • ... if x evaluates to a value of type T* that is equal to &a for some variable a of type T, then *x is an alias for a.

Now everything follows:

int x = 123;

x is a variable of type int. Its value is 123.

int* y = &x;

y is a variable of type int*. x is a variable of type int. So &x is a value of type int*. Therefore we can store &x in y.

*y = 456;

y evaluates to the contents of variable y. That's a value of type int*. Applying * to a value of type int* gives a variable of type int. Therefore we can assign 456 to it. What is *y? It is an alias for x. Therefore we have just assigned 456 to x.

int** z = &y;

What is z? It's a variable of type int**. What is &y? Since y is a variable of type int*, &y must be a value of type int**. Therefore we can assign it to z.

**z = 789;

What is **z? Work from the inside out. z evaluates to an int**. Therefore *z is a variable of type int*. It is an alias for y. Therefore this is the same as *y, and we already know what that is; it's an alias for x.

No really, what's the point?

Here, I have a piece of paper. It says 1600 Pennsylvania Avenue Washington DC. Is that a house? No, it's a piece of paper with the address of a house written on it. But we can use that piece of paper to find the house.

Here, I have ten million pieces of paper, all numbered. Paper number 123456 says 1600 Pennsylvania Avenue. Is 123456 a house? No. Is it a piece of paper? No. But it is still enough information for me to find the house.

That's the point: often we need to refer to entities through multiple levels of indirection for convenience.

That said, double pointers are confusing and a sign that your algorithm is insufficiently abstract. Try to avoid them by using good design techniques.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
10

A double-pointer, is simply a pointer to a pointer. A common usage is for arrays of character strings. Imagine the first function in just about every C/C++ program:

int main(int argc, char *argv[])
{
   ...
}

Which can also be written

int main(int argc, char **argv)
{
   ...
}

The variable argv is a pointer to an array of pointers to char. This is a standard way of passing around arrays of C "strings". Why do that? I've seen it used for multi-language support, blocks of error strings, etc.

Don't forget that a pointer is just a number - the index of the memory "slot" inside a computer. That's it, nothing more. So a double-pointer is index of a piece of memory that just happens to hold another index to somewhere else. A mathematical join-the-dots if you like.

This is how I explained pointers to my kids:

Imagine the computer memory is a series of boxes. Each box has a number written on it, starting at zero, going up by 1, to however many bytes of memory there is. Say you have a pointer to some place in memory. This pointer is just the box number. My pointer is, say 4. I look into box #4. Inside is another number, this time it's 6. So now we look into box #6, and get the final thing we wanted. My original pointer (that said "4") was a double-pointer, because the content of its box was the index of another box, rather than being a final result.

It seems in recent times pointers themselves have become a pariah of programming. Back in the not-too-distant past, it was completely normal to pass around pointers to pointers. But with the proliferation of Java, and increasing use of pass-by-reference in C++, the fundamental understanding of pointers declined - particularly around when Java became established as a first-year computer science beginners language, over say Pascal and C.

I think a lot of the venom about pointers is because people just don't ever understand them properly. Things people don't understand get derided. So they became "too hard" and "too dangerous". I guess with even supposedly learned people advocating Smart Pointers, etc. these ideas are to be expected. But in reality there a very powerful programming tool. Honestly, pointers are the magic of programming, and after-all, they're just a number.

Kingsley
  • 14,398
  • 5
  • 31
  • 53
1

In many situations, a Foo*& is a replacement for a Foo**. In both cases, you have a pointer whose address can be modified.

Suppose you have an abstract non-value type and you need to return it, but the return value is taken up by the error code:

error_code get_foo( Foo** ppfoo )

or

error_code get_foo( Foo*& pfoo_out )

Now a function argument being mutable is rarely useful, so the ability to change where the outermost pointer ppFoo points at is rarely useful. However, a pointer is nullable -- so if get_foo's argument is optional, a pointer acts like an optional reference.

In this case, the return value is a raw pointer. If it returns an owned resource, it should usually be instead a std::unique_ptr<Foo>* -- a smart pointer at that level of indirection.

If instead, it is returning a pointer to something it does not share ownership of, then a raw pointer makes more sense.

There are other uses for Foo** besides these "crude out parameters". If you have a polymorphic non-value type, non-owning handles are Foo*, and the same reason why you'd want to have an int* you would want to have a Foo**.

Which then leads you to ask "why do you want an int*?" In modern C++ int* is a non-owning nullable mutable reference to an int. It behaves better when stored in a struct than a reference does (references in structs generate confusing semantics around assignment and copy, especially if mixed with non-references).

You could sometimes replace int* with std::reference_wrapper<int>, well std::optional<std::reference_wrapper<int>>, but note that is going to be 2x as large as a simple int*.

So there are legitimate reasons to use int*. Once you have that, you can legitimately use Foo** when you want a pointer to a non-value type. You can even get to int** by having a contiguous array of int*s you want to operate on.

Legitimately getting to three-star programmer gets harder. Now you need a legitimate reason to (say) want to pass a Foo** by indirection. Usually long before you reach that point, you should have considered abstracting and/or simplifying your code structure.

All of this ignores the most common reason; interacting with C APIs. C doesn't have unique_ptr, it doesn't have span. It tends to use primitive types instead of structs because structs require awkward function based access (no operator overloading).

So when C++ interacts with C, you sometimes get 0-3 more *s than the equivalent C++ code would.

Farzad Karimi
  • 770
  • 1
  • 12
  • 31
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I cannot follow the logic. E.g. how is `std::unique_ptr*` useful, and why you are talking about returning it, while you already occupied return type with error code? – Mikhail Feb 27 '18 at 21:06
  • @Mikhail `error_code get_foo( std::unique_ptr* here )` "returns" its "return value" through a pointer argument, while its actual return is taken up by `error_code`. This pattern is fromalized in stuff like COM and IDL, where exceptions are not used and semantic return values are always pointers, while syntactic return values are error codes. – Yakk - Adam Nevraumont Feb 27 '18 at 21:11
0

The use is to have a pointer to a pointer, e.g., if you want to pass a pointer to a method by reference.

Mureinik
  • 297,002
  • 52
  • 306
  • 350
0

In C++, if you want to pass a pointer as an out or in/out parameter, you pass it by reference:

int x;
void f(int *&p) { p = &x; }

But, a reference can't ("legally") be nullptr, so, if the pointer is optional, you need a pointer to a pointer:

void g(int **p) { if (p) *p = &x; }

Sure, since C++17 you have std::optional, but, the "double pointer" has been idiomatic C/C++ code for many decades, so should be OK. Also, the usage is not so nice, you either:

void h(std::optional<int*> &p) { if (p) *p = &x) }

which is kind of ugly at the call site, unless you already have a std::optional, or:

void u(std::optional<std::reference_wrapper<int*>> p) { if (p) p->get() = &x; }

which is not so nice in itself.

Also, some might argue that g is nicer to read at the call site:

f(p);
g(&p); // `&` indicates that `p` might change, to some folks
srdjan.veljkovic
  • 2,468
  • 16
  • 24
0

What real use does a double pointer have?

Here is practical example. Say you have a function and you want to send an array of string params to it (maybe you have a DLL you want to pass params to). This can look like this:

#include <iostream>

void printParams(const char **params, int size)
{
    for (int i = 0; i < size; ++i)
    {
        std::cout << params[i] << std::endl;
    }
}

int main() 
{
    const char *params[] = { "param1", "param2", "param3" };
    printParams(params, 3);

    return 0;
}

You will be sending an array of const char pointers, each pointer pointing to the start of a null terminated C string. The compiler will decay your array into pointer at function argument, hence what you get is const char ** a pointer to first pointer of array of const char pointers. Since the array size is lost at this point, you will want to pass it as second argument.

Killzone Kid
  • 6,171
  • 3
  • 17
  • 37
0

One case where I've used it is a function manipulating a linked list, in C.

There is

struct node { struct node *next; ... };

for the list nodes, and

struct node *first;

to point to the first element. All the manipulation functions take a struct node **, because I can guarantee that this pointer is non-NULL even if the list is empty, and I don't need any special cases for insertion and deletion:

void link(struct node *new_node, struct node **list)
{
    new_node->next = *list;
    *list = new_node;
}

void unlink(struct node **prev_ptr)
{
    *prev_ptr = (*prev_ptr)->next;
}

To insert at the beginning of the list, just pass a pointer to the first pointer, and it will do the right thing even if the value of first is NULL.

struct node *new_node = (struct node *)malloc(sizeof *new_node);
link(new_node, &first);
Simon Richter
  • 28,572
  • 1
  • 42
  • 64
  • Wouldn't that be better expressed by defining a `struct list { struct node * head; };`? Then you take a `struct list *`. As a bonus, should you at some point need to store more stuff (eg: list length, or tail pointer), you won't have to change all your signatures. – spectras Feb 27 '18 at 21:33
  • @spectras, that would still require me to either special-case insertion of the first element, or form a pointer-to-pointer. I'm using pointer-to-pointer in that implementation because it allows me to insert in arbitrary places. – Simon Richter Feb 27 '18 at 21:36
0

Multiple indirection is largely a holdover from C (which has neither reference nor container types). You shouldn't see multiple indirection that much in well-written C++, unless you're dealing with a legacy C library or something like that.

Having said that, multiple indirection falls out of some fairly common use cases.

In both C and C++, array expressions will "decay" from type "N-element array of T" to "pointer to T" under most circumstances1. So, assume an array definition like

T *a[N]; // for any type T

When you pass a to a function, like so:

foo( a );

the expression a will be converted from "N-element array of T *" to "pointer to T *", or T **, so what the function actually receives is

void foo( T **a ) { ... }

A second place they pop up is when you want a function to modify a parameter of pointer type, something like

void foo( T **ptr )
{
  *ptr = new_value();
}

void bar( void )
{
  T *val;
  foo( &val );
}

Since C++ introduced references, you probably won't see that as often. You'll usually only see that when working with a C-based API.

You can also use multiple indirection to set up "jagged" arrays, but you can achieve the same thing with C++ containers for much less pain. But if you're feeling masochistic:

T **arr;

try
{
  arr = new T *[rows];
  for ( size_t i = 0; i < rows; i++ )
    arr[i] = new T [size_for_row(i)];
}
catch ( std::bad_alloc& e )
{
  ...
}

But most of the time in C++, the only time you should see multiple indirection is when an array of pointers "decays" to a pointer expression itself.


  1. The exceptions to this rule occur when the expression is the operand of the sizeof or unary & operator, or is a string literal used to initialize another array in a declaration.

John Bode
  • 119,563
  • 19
  • 122
  • 198
  • Re, "C ...[has no] container types." You mean, the C _standard library_ has none. After all, there's no container types in the C++ language either: Those are all implemented by library routines. Anyway, nothing ever stopped a smart C programmer from implementing his/her own container types, and nothing ever stopped an even smarter programmer from using container implementations that somebody else already implemented and debugged. (Though, to be fair, I didn't know to call them "containers" until decades later, after I started working in Java.) – Solomon Slow Feb 28 '18 at 01:50