3

According to the C++03 Standard (5.3.4/7):

When the value of the expression in a direct-new-declarator is zero, the allocation function is called to allocate an array with no elements.

By my reading, this means that this code is legal and has a specified effect:

#include <iostream>
#include <string>
using namespace std;

class A
{
public:
    A() : a_(++aa_) {};
    int a_;
    static int aa_;
};
int A::aa_ = 0;

int main()
{
    A* a = new A[0];
    // cout << "A" << a->a_ << endl; // <-- this would be undefined behavior
}

When I run this code under the debugger, I see that A's constructor is never called. new does not throw, and returns a non-null, apparently valid pointer. However, the value at a->a_ is uninitialized memory.

Questions:

  1. In the above code, what does a actually point to?
  2. What does is mean to "allocate an array with no elements?"
  3. Of what practical use is it to allocate an array with zero elements?
John Dibling
  • 99,718
  • 31
  • 186
  • 324

3 Answers3

5

In the above code, what does a actually point to?

Points to a zero element array.

What does is mean to "allocate an array with no elements?"

It means that we get a valid array, but of zero size. We can't access the values into it since there are none, but we now that each zero-sized array points to a different location and we can even make an iterator to past-the-end &a[0]. So we can use it just like we use every other array.

Of what practical use is it to allocate an array with zero elements?

It just saves you from checking for n = 0 every time you call new. Note that a static array of zero-size is illegal, since static arrays have static sizes that come from constant expressions. And furthermore, allows you to call any standard or custom algorithm taking a pair of iterators.

K-ballo
  • 80,396
  • 20
  • 159
  • 169
4

Your a points (validly) to zero consecutive elements, and zero elements have been constructed. The expression a->a_ is the same as a[0].a_, which is out of bounds and hence undefined behaviour.

Furthermore, the statement "the value at a->a_ is uninitialized memory" doesn't seem to make sense: a value is a value, and memory is memory -- a value cannot be memory. Since we already established that you must never dereference a, it is meaningless to ask for any sort of value of a variable to which a points: a just doesn't point anywhere useful.

It is still perfectly valid (and expected?!) to say delete[] a; to release the allocated memory and destroy all of the zero objects.

(Note that C's malloc function can also be called with argument 0, but there are no guarantees about its return value other that you can free it. In C++, ::operator new[] is actually required to return a distinct, non-null value every time (subject to reusing values that have been passed to ::operator delete[]).

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • +1: Everything you've said makes perfect sense, of course. Any comment of what practical use it is to allocate an array of zero elements? – John Dibling Jun 17 '12 at 19:53
  • 3
    @JohnDibling: It means you don't need to add extra checks in certain algorithms. You can write generically and never worry about whether there are strange fringe cases you didn't consider. Note that `malloc` has similar semantics. – Kerrek SB Jun 17 '12 at 20:00
  • OK, that seems like a reasonable explanation. The idea of not handling fringe cases explicitly rubs me the wrong way, but the Standard never said I had to do this. :) – John Dibling Jun 17 '12 at 20:09
3

What it comes down to is this: new whatever[0] results in returning a valid address, different from that of any other currently-allocated object, but which you cannot dereference.

The typical use for it is with code that allocates varying sizes of arrays, so at some times you might have an array with a positive number of elements, but at other times an array with zero elements. Even though you can't dereference it, having a "normal" (valid) pointer instead of (for example) a null pointer can still simplify quite a bit of code.

Just for one example, consider the typical case of storing the address of the start of your array, and the address one beyond the end of the array (and having loops that traverse from one to the other). With a null pointer, you can't do arithmetic like that, so anything that dealt with the one-past-the-end pointer would need special casing for a null pointer. In the case of a valid, non-null pointer that you can't dereference, you can still form the starting and ending addresses without any special cases. Instead of special-case code to execute the loops only for non-null pointers, you write normal loops that execute zero iterations when the array is empty.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • Apparently, "pointer to element one-past-the-end" is kind of *undefined-behavior*. See http://stackoverflow.com/questions/10473573/why-is-out-of-bounds-pointer-arithmetic-undefined-behaviour . Just FYI though. – quetzalcoatl Oct 12 '14 at 10:04
  • 2
    @quetzalcoatl: That's a pointer *two* past the end. Zero through the end are valid pointers. It's valid to *form* a pointer one past the end, and it gives valid comparisons to pointers in the array (you just can't dereference it). Pointers past that, however, can't even be *formed* legitimately. – Jerry Coffin Oct 13 '14 at 04:25