How do smart pointers choose between delete and delete[]?
Asked Answered
S

5

34

Consider:

delete new std :: string [2];
delete [] new std :: string;

Everyone knows the first is an error. If the second wasn't an error, we wouldn't need two distinct operators.

Now consider:

std :: unique_ptr <int> x (new int [2]);
std :: unique_ptr <int> y (new int);

Does x know to use delete[] as opposed to delete?


Background: this question floated through my head when I thought array type qualification of pointers would be a handy language feature.

int *[] foo = new int [2]; // OK
int *   bar = new int;     // OK
delete [] foo;             // OK
delete bar;                // OK
foo = new int;             // Compile error
bar = new int[2];          // Compile error
delete foo;                // Compile error
delete [] bar;             // Compile error
Solent answered 20/1, 2012 at 11:44 Comment(3)
Instead of modifying the array type qualifier (which already exists) to work the way you show, it would be better to just eliminate all the implicit type conversions that cause all the problems with arrays. For example new int[2] ought to return a value of type int (*)[2]. Anyway, it's too late to fix all the problems with native arrays, but you can use std::array and avoid ever having to deal with them. std::array works like native arrays should work and you never have to use new[] or delete[].Ratan
@bames53, There is a limitation with std::array. You can do new int[n], but you can't do std::array<int,n>. (Unless n is known at compile time). But still, I avoid native arrays!#Tavish
@AaronMcDaid Yeah, use std::vector if you need dynamic allocation.Ratan
L
32

Unfortunately, they don't know what delete to use therefore they use delete. That's why for each smart pointer we have a smart array counterpart.

std::shared_ptr uses delete
std::shared_array uses delete[]

So, your line

std :: unique_ptr <int> x (new int [2]);

actually causes undefined behavior.

Incidentally, if you write

std :: unique_ptr<int[]> p(new int[2]);
                     ^^

then delete[] will be used since you've explicitly requested that. However, the following line will still be UB.

std :: unique_ptr<int[]> p(new int);

The reason that they can't choose between delete and delete[] is that new int and new int[2] are exactly of the same type - int*.

Here's a related question of using correct deleters in case of smart_ptr<void> and smart_ptr<Base> when Base has no virtual destructor.

Lianna answered 20/1, 2012 at 11:47 Comment(13)
@tenfour: Because the types are exactly the same!Lianna
Because they can't detect std::shared_ptr<int> make (int *i) {return i;}Solent
@tenfour: (new int) and (new int[2]) both have type int*, there's no way to distinguish them for template specialization.Spermic
"if you write std :: unique_ptr<int[]> p(new int[2]);" - this is because the standard defines a partial specialization of unique_ptr for array types. So the constructor takes int* rather than int (*)[] as you'd otherwise expect, and there are some other differences (for example no operator->, but operator[] instead).Teletypesetter
typedef <typename T> using shared_array = shared_ptr<T[]>;Solent
@spraff: Nope, shared_ptr has no specialization for T[].Datum
@Xeo: so why's that then? Something to do with the fact that unique_ptr has the deleter type in the template parameters, whereas shared_ptr uses type erasure? I suppose you write shared_ptr<int> p(new int[2], std::default_delete<int[]>()); or thereabouts, but then you don't get operator[].Teletypesetter
@Steve: I have no idea about the rationale, but I'd believe it's something like that.Datum
Of course shared_array isn't needed because you can use std::array or std::vector, and these work just fine with smart pointers and new and delete. There's no need to use new[] or delete[] anymore.Ratan
-1: For talking about std::shared_array that doesn't exist.Kerakerala
@bames53: by exactly the same argument there's no need for the array specialization of unique_ptr, and yet somehow that slipped through into the standard. So I don't think it's a matter of course that shared_array didn't make it, whether it's needed or not.Teletypesetter
@SteveJessop Yes, the same argument applies to the array specialization of unique_ptr. I would say not including shared_array actually was the natural thing (as far as I know no one thought enough of it to bother to include it in any proposal). The fact that the array specialization of unique_ptr was included is the oddity.Ratan
std::array is not a reasonable replacement for shared_ptr<type[]> when you don't need the size parameter since it wastes memory.Tincal
A
6

There is no "magical" way to detect whether a int* refers to:

  • a single heap allocated integer
  • a heap allocated array
  • an integer in a heap allocated array

The information was lost by the type system and no runtime method (portable) can fix it. It's infuriating and a serious design flaw (*) in C that C++ inherited (for the sake of compatibility, some say).

However, there are some ways of dealing with arrays in smart pointers.

First, your unique_ptr type is incorrect to deal with an array, you should be using:

std::unique_ptr<int[]> p(new int[10]);

which is meant to call delete[]. I know there is talk of implementing a specific warning in Clang to catch obvious mismatches with unique_ptr: it's a quality of implementation issue (the Standard merely says it's UB), and not all cases can be covered without WPA.

Second, a boost::shared_ptr can have a custom deleter which could if you design it to call the correct delete[] operator. However, there is a boost::shared_array especially designed for this. Once again, detection of mismatches is a quality of implementation issue. std::shared_ptr suffers the same issue (edited after ildjarn's remark).

I agree that it's not pretty. It seems so obnoxious that a design flaw (*) from the origins of C haunts us today still.

(*) some will say that C leans heavily toward avoiding overhead and this would have added an overhead. I partly disagree: malloc always know the size of the block, after all.

Amaryllidaceous answered 20/1, 2012 at 12:10 Comment(14)
I think that the reason in C is a bit more complex. If dynamically allocated arrays yielded different types (as auto allocated do size being part of the type) then you could not write a strlen function to manage all liberals, for example, and there are no overloads in C. The alternative would be having a single array type that contained the size as a member, but that would make mapping hardware with arrays hard (were would you store the size? maintain size and a pointer to the data? that would leave us in square one: a pointer refers to either one or more elements...)Loeb
BTW: I the list of options, don't forget that a pointer can also refer to auto or static memory, or shared memory, hardware addresses... Each of which has the two possibilities: array or single element (which can belong to an array) +1Loeb
My suggestion in the "background" section of the question would be a no-overhead solution, but not backwards-compatible.Solent
@DavidRodríguez-dribeas: I think this is a false issue. delete[] knows how many elements were built as it invokes the destructors. The typical way of dealing with this would be using tail padding: struct Array { size_t size; char content[]; }; and return a pointer to the content. This means that if you assume it is an array, then you know the size. Of course it does not help with the pointer/array distinction, but in C there is no solution. In C++, a debug implementation could "remember" if the allocation was performed by new or new[] and check that the correct delete is invoked.Amaryllidaceous
C, having no function overloading and no templates, doesn't really need strict type distinctions as much as C++ does.Transcription
@dan04: it's a point of view. Personally I think that with a proper array built-in in C, lots of memory overflows would have been avoided. Too many people forget to pass the size along the pointer to the buffer, or make mistakes when passing it; typical: memcpy(buffer, sizeof(buffer), src) when buffer is not an array but a pointer...Amaryllidaceous
@MatthieuM. I agree that in C++ it is a non-issue, and also that having a proper array type in C would have helped removing many bugs, but in a systems language you need to be able to access raw areas in memory as if they were arrays and do so without having to embed a size that might not fit the memory layout of the hardware. Now, that does not mean that there could not be an array type and a buffer (or raw_array) type. Then again, you might need to duplicate otherwise similar code to handle the two variants... I am not argueing, just bringing up potential design issues.Loeb
@DavidRodríguez-dribeas: Yes, I understand the concern of mapping directly to raw memory. Still I think this is fairly easily address by defining a Range structure, that has either a size and pointer or just two pointers as in C++ half-open range. Then you are free to make your functions against that interface :)Amaryllidaceous
@Matthieu You're making the mistake of assuming that the purpose of C is to be a high-level language that protects you from the consequences of poor programming. That is not true. C is there to make it less painful to write low-level code; some of the enormously nasty things you can do in C are there because they're needed to make complex apps work at all. (This was especially true on smaller machines where the cost of overheads could mean the difference between something that would fit in memory and something that wouldn't.)Agrarian
@DonalFellows: I am sorry... where did "overloads" popped up in the conversation ? While I really like them (though not to the extent that I support the awkward way they are introduced in C11), I don't think it came up here yet.Amaryllidaceous
@Matthieu The real key is whether the other language (C in this case) will pass the result/argument correctly. If it's just an opaque token, it doesn't have to interpret it (and in fact, long experience tells me that it turns out to be better to put all interpretation behind an API). The problem only really comes when you're passing around something larger than a machine word, i.e. a struct or object, as that has certainly been a consistency problem in the past and it maximizes the potential for misunderstanding of object sizes (critical).Agrarian
The long and short of it is that passing around an opaque-but-otherwise-ordinary pointer (which could be to anything) is easier than passing around an object by value. Yes, it has significant downsides in memory management; that's what you have to put up with.Agrarian
"std::shared_ptr provides the same syntax that std::unique_ptr does." If by this you mean that std::shared_ptr<int[]> p(new int[10]); will work, it doesn't.Rociorock
@ildjarn: right, I thought it did, though with hindsight it could be non-trivial: what if someone grabs a reference within the array ? (current with pointers).Amaryllidaceous
T
3

From Microsoft's documentation:

(A partial specialization unique_ptr<Type[]> manages array objects allocated with new[], and has the default deleter default_delete<Type[]>, specialized to call delete[] _Ptr.)

I added the two final square brackets, seems like a typo as it doesn't make sense without them.

Tarsia answered 20/1, 2012 at 11:48 Comment(3)
Reading Armen Tsirunyan's answer, I don't know what to make of this. I'll let it stand, as an interesting point. Perhaps I'm missing something fundamental.Tarsia
I think MSDN means that if you write shared_ptr<int[]> p(new int[2]) then delete[] will be used. But if you write shared_ptr<int> p (new int[2]) this will result in UB.Lianna
I suppose you could write something like shared_ptr<int>i=shared_ptr<int>::allocate() and shared_ptr<int[]>i=shared_ptr<int[]>::allocate(n) and prohibit construction from raw pointers. (For your own shared_ptr, I mean.)Solent
A
3

std::unique_ptr is not meant for array as I quote latest boost document:

Normally, a shared_ptr cannot correctly hold a pointer to a dynamically allocated array. See shared_array for that usage.

If you want to memory management for array of pointer, you have a few options depend on your requirement:

  1. Use boost::shared_array
  2. Use std::vector of boost::shared_ptr
  3. Use boost pointer container like boost::ptr_vector
Augsburg answered 20/1, 2012 at 12:7 Comment(3)
I suppose the mutual exclusion of member operator[](size_t) and operator*() would be a good clue.Solent
You're backing up your claim about unique_ptr with a quote about shared_ptr. Weird.Vaios
std::unique_ptr is specialized for array types and will use delete[] if you specify one (e.g., std::unique_ptr<int[]>). std::vector is a better option though. I don't see any need for shared_arrayRatan
Z
0

Nicolai Josuttis ⟶ Note that the default deleter provided by std::shared_ptr calls delete, not delete[]. This means that the default deleter is appropiate only if a shared pointer owns a single object created with new. Unfortunately, creating a std::shared_ptr for an array is possible but wrong:

std::shared_ptr<int>(new int[10]) //error but compiles

So if you use new[] to create an array of objects you have to define your own deleter. For example

std::shared_ptr<int> ptr(new int[10], 
                         [](int* p){ delete[] p; });

or

std::shared_ptr<int> ptr(new int[10], 
                         std::default_delete<int[]>());

For me this is the best solution!

Zygapophysis answered 27/2, 2020 at 12:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.