1

I read Are negative array indexes allowed in C? and found it interesting that negative values can be used for the index of an array. I tried it again with the c++11 unique_ptr and it works there as well! Of course the deleter must be replaced with something which can delete the original array. Here is what it looks like:

#include <iostream>
#include <memory>

int main()
{
    const int min = -23; // the smaller valid index
    const int max = -21; // the highest valid index
    const auto deleter = [min](char* p)
    {
        delete [](p+min);
    };
    std::unique_ptr<char[],decltype(deleter)> up(new char[max-min+1] - min, deleter);

    // this works as expected
    up[-23] = 'h'; up[-22] = 'i'; up[-21] = 0;
    std::cout << (up.get()-23) << '\n'; // outputs:hi
}

I'm wondering if there is a very, very small chance that there is a memory leak. The address of the memory created on the heap (new char[max-min+1]) could overflow when adding 23 to it and become a null pointer. Subtracting 23 still yields the array's original address, but the unique_ptr may recognize it as a null pointer. The unique_ptr may not delete it because it's null.

So, is there a chance that the previous code will leak memory or does the smart pointer behave in a way which makes it safe?

Note: I wouldn't actually use this in actual code; I'm just interested in how it would behave.

Community
  • 1
  • 1
Ryan
  • 2,378
  • 1
  • 19
  • 29

2 Answers2

4

Edit: icepack brings up an interesting point, namely that there are only two valid pointer values that are allowed in pointer arithmetic:

§5.7 [expr.add] p5

If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

As such, the new char[N] - min of your code already invokes UB.


Now, on most implementations, this will not cause problems. The destructor of std::unique_ptr, however, will (pre-edit answer from here on out):

§20.7.1.2.2 [unique.ptr.single.dtor] p2

Effects: If get() == nullptr there are no effects. Otherwise get_deleter()(get()).

So yes, there is a chance that you will leak memory here if it indeed maps to whatever value represents the null pointer value (most likely 0, but not necessarily). And yes, I know this is the one for single objects, but the array one behaves exactly the same:

§20.7.1.3 [unique.ptr.runtime] p2

Descriptions are provided below only for member functions that have behavior different from the primary template.

And there is no description for the destructor.

Community
  • 1
  • 1
Xeo
  • 129,499
  • 52
  • 291
  • 397
  • The deleter in the snippet actually tries to access the correct pointer to the beginning of the allocated array, so there shouldn't be a problem. However, there is another issue - see my answer. – SomeWittyUsername Oct 29 '12 at 06:37
  • 1
    @icepack: The deleter is not even called if `get() == nullptr`. – Xeo Oct 29 '12 at 06:37
  • In order for it to be null, the `unique_ptr` must have no ownership of a managed object and that's not the case here. – SomeWittyUsername Oct 29 '12 at 06:46
  • @icepack: If the `new char[N] - min` yields `0`, it is. – Xeo Oct 29 '12 at 06:48
  • Not sure about that. This actually depends on `unique_ptr` implementation - it's very possible that ownership is still valid as far as `unique_ptr` concerns. E.g., it may mean owning an object starting from address 0 (Besides, practically I don't think there is a modern computer architecture that will allow returning address 23 as a valid pointer to allocated memory) – SomeWittyUsername Oct 29 '12 at 06:58
  • @icepack: I don't think it's an issue on ARM per se (the ARM architecture doesn't have a fixed memory map, and some implementations do have RAM at offset 0.) – MSalters Oct 29 '12 at 07:38
  • @icepack: Even if `unique_ptr` thinks it manages an object that resides at the null pointer address, the end result is the same - the deleter will simply not get called in that case. – Xeo Oct 29 '12 at 14:02
3

new char[max-min+1] doesn't allocate memory on the stack but rather on heap - that's how standard operator new behaves. The expression max-min+1 is evaluated by the compiler and results in 3, so eventually this expression is equal to allocating 3 bytes on the heap. No problem here.

However, subtracting min results in pointer which is 23 bytes beyond the beginning of the allocated memory returned by new and since in new you allocated only 3 bytes, this will definitely point to a location not owned by you --> anything following will result in undefined behavior.

SomeWittyUsername
  • 18,025
  • 3
  • 42
  • 85
  • Thanks, I actually wanted to say 'heap' instead of 'stack'. Nice catch! Also, are pointers pointing to something not owned considered undefined behaviour even if you don't dereference them? For example, `vector.end()` points past the end of a vector and that's okay as long as it's not dereferenced. – Ryan Oct 29 '12 at 06:26
  • As long as you don't dereference the pointer, it's ok: the pointer is just a number. In your code you use this pointer as an input parameter to the `unique_ptr` and what really happens there depends on the implementation of `unique_ptr` class. – SomeWittyUsername Oct 29 '12 at 06:28
  • @Ryan Dereferencing is not even a consideration, it's computing pointers that is the problem. When doing pointer arithmetic, you must stay within the bounds of the array or point to one-past-the end. And it's not necessary to bring iterators into this. – Luc Danton Oct 29 '12 at 06:29
  • No, it's not okay even if you do not dereference the pointer. The sole act of creating that pointer makes problems, see my now edited answer (thanks for bringing this up, +1). – Xeo Oct 29 '12 at 06:36
  • I think we have a bit of confusion here. I was referring to a standard pointers (C-type) and for such there is no problem to set to any value. Of course, that's not the case for smart pointers. – SomeWittyUsername Oct 29 '12 at 06:49
  • @Xeo I've read the excerpt you provided - it appears that you're right but that sounds like a very strange and problematic behavior: e.g., we can have a non-array structure pointed by a pointer that's not in the scope of an array. – SomeWittyUsername Oct 29 '12 at 07:03
  • @icepack For the purposes of those operators it is acceptable to treat objects as if they were arrays of that type with size 1. So if you point to an actual object, you're good to go -- in fact you're even allowed to increment that pointer and point to one-past-the-end of the conceptual array. – Luc Danton Oct 29 '12 at 08:05