1

I was shocked to notice that the following code is working correctly on my machine, i.e. constructing and destroying all 25 objects of the two-dimensional array in the correct order.

#include <iostream>

class C {
public:
  C() { std::cout << '+' << this << "\n"; }
  ~C() { std::cout << '-' << this << "\n"; }
};

typedef C T[5][5];

int main() {
  void *t = new T;
  delete (T *) t;
  return 0;
}

Is it indeed a valid way of allocating and deallocating multi-dimensional arrays according to the standard? Are operators new and delete actually required to introspect multi-dimensional array types recursively in this manner?

NoQ
  • 75
  • 6
  • Are you shocked that this works if you just do `T t;`? The same 25 objects have to be constructed and destructed, right? – David Schwartz Apr 26 '18 at 20:58
  • I have a feeling your compiler is optimizing away the new/delete – Brady Dean Apr 26 '18 at 21:02
  • This is not a matrix. This is an array of 5 x 5 = 25 elements. – Grzegorz Apr 26 '18 at 21:05
  • My compiler is not optimizing away the new/delete because it's actually printing 50 prints. – NoQ Apr 26 '18 at 21:07
  • @jxh An array is just a pointer to the first element, correct? I think the confusion is that an array is allocated, but being correctly deallocated by delete, not delete[]. It should be undefined behavior but he could be getting lucky – Brady Dean Apr 26 '18 at 21:08
  • @NoQ The compiler may remove new/delete if the program has the same behavior. It would just stack allocate the objects instead – Brady Dean Apr 26 '18 at 21:08
  • The reason why i'm shocked is that i no longer understand the difference between `new` and `new[]` and i'm trying to figure out which one of these correspond to the "`new[][]`" presented above. – NoQ Apr 26 '18 at 21:10
  • @NoQ you may still [edit] your question to clarify what it is you are really asking. – Drew Dormann Apr 26 '18 at 21:11
  • I think it's pretty clear what i'm asking about: "Is it indeed a valid way of allocating and deallocating multi-dimensional arrays according to the standard?". – NoQ Apr 26 '18 at 21:13
  • 1
    https://stackoverflow.com/a/16239446/315052 – jxh Apr 26 '18 at 21:14
  • @BradyDean: The compiler may treat the `[5][5]` as syntactic sugar that is mostly enforced at compile time. Since what is being destructed by `delete` is a pointer to an array, the array deletion may have been silently applied. – jxh Apr 26 '18 at 21:18
  • @jxh That would make sense. It should be UB, at least according to cppreference. gcc is only giving me a warning about deleting void* – Brady Dean Apr 26 '18 at 21:20
  • 3
    `delete (T *) t` is actually UB, that is not the type of `t`. `t` is actually `C (*)[5]`. – jxh Apr 26 '18 at 21:20
  • clang gives me the following warning: `'delete' applied to a pointer-to-array type 'T *' (aka 'C (*)[5][5]') treated as 'delete[]'` – NoQ Apr 26 '18 at 21:25
  • Use `auto` instead of `void *`. Get rid of the cast in the call to `delete`. – jxh Apr 26 '18 at 21:27
  • ...and the type of the new-expression is indeed `C (*)[5]` in it. – NoQ Apr 26 '18 at 21:28
  • @jxh thanks! I think i'm starting to understand this. – NoQ Apr 26 '18 at 21:31
  • @DrewDormann incorrect; the type of the pointer being deleted must match the type returned by its `new` (or a base class of, with virtual destructor) – M.M Apr 26 '18 at 22:54
  • @RemyLebeau: https://tio.run/##fY3NCoJAFIX3PsXBNoW4sKU/bXwFoUW1GMarDeiMOHeIEHv0pkladzlw4PDxXTlNqRyE7v1OaTm4llAqY3kmMZ58aCcZNZYI4WrsD1hguc1zaRyjLMF3Zb8dI7nquMC6ka//aPpD1yKK@DlRS12QN5fjLSRsSjNGofQm2YTCsQGjgqYHmmLbWhqICVxEq/dv2Q2itz490zyb2afhdSWTJMs@ – jxh Apr 27 '18 at 00:37
  • @RemyLebeau: https://tio.run/##rZHBasMwDIbveQrRXbyFHLqjk@7SVyjs0JVgHKUzOLax5Y21ZI8@z/WgDAZhjAmBQPz6fiFJ5xqphTmmG2WkjgNCp2wgj2J6SLlGSbCFcwU5tsBu4QyBBs6ljQRdB/SswqWuoH4yqxbmonxfljbfpC9WDXAH1qEXZD0YfAUW1Al7gnD6Aen7MRrZ9wVUMB4pegOcT0JrK1keWmDvD/9Lv7IH1EgI7MvT/QLN@egRmVsEXvb9I3Juq4reHA445tft9veHnLmnDMEklCkvKsYikgWCTTn@ri29qz211ZzShxy1OIbUPKL31qcmb7KRdb1efwI – jxh Apr 27 '18 at 00:49

1 Answers1

4

This code is not correct, because:

  1. The subexpression of the delete operator does not have the correct type.
  2. Since the allocated type is an array type, you must use the delete[] syntax.

The correct code would be:

// C and T as in the OP.

int main() {
    C (*t)[5] = new T; // or just: auto t = new T;
    delete [] t;
}

First, the type of new T is C (*)[5], not T* (or C*, etc.) [expr.new]/5:

When the allocated object is an array (that is, the noptr-new-declarator syntax is used or the new-type-id or type-id denotes an array type), the new-expression yields a pointer to the initial element (if any) of the array. [ Note: Both new int and new int[10] have type int* and the type of new int[i][10] is int (*)[10] — end note ]

The same type must be used in the subexpression of delete[], and since the allocated object is an array, the delete[] operator must be used. [expr.delete]/2:

In a single-object delete expression, the value of the operand of delete may be a null pointer value, a pointer to a non-array object created by a previous new-expression, or a pointer to a subobject representing a base class of such an object. If not, the behavior is undefined. In an array delete expression, the value of the operand of delete may be a null pointer value or a pointer value that resulted from a previous array new-expression. If not, the behavior is undefined. [ Note: This means that the syntax of the delete-expression must match the type of the object allocated by new, not the syntax of the new-expression. — end note ]

In particular, if the pointer value resulting from your new-expression was cast to a different type such as T*, the pointer value of that type is not the pointer value which resulted from the new-expression. Except as noted for deleting a single polymorphic class object, the type of pointer given to delete or delete[] needs to be the same as the type of the new-expression.

As far as initializing and destroying goes: No matter how an array object is created, initializing the array object involves initializing each element of that array. When you have an array of arrays, this applies recursively. So the expression new T creates and initializes one C[5][5] array object, which means initializing five C[5] objects, and each of those initializations means initializing five C objects (for a total of 25 C constructor calls).

And no matter how an array object was created, a valid way of destroying that array object involves destroying each element of that array, in reverse order. So the delete [] t; statement destroys the entire array whose first element is *t - this means destroying all five elements of type C[5], and each of those destructions means destroying five C objects (for a total of 25 ~C destructor calls).

Note the compiler knows from the type of t that the elements of the array have type C[5], but it doesn't specify the major size of the array. The same expression would be valid if *t were actually, for example, the first element of a C[8][5] array instead. Behind the scenes, many compilers will solve this issue by storing a count of array elements next to any array allocated by a new-expression, so that the later delete-expression can determine how many elements need to be destroyed.

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • One way to dynamically allocate one `T` would be to `new T[1]`. Still requires `delete[]` though. – jxh Apr 26 '18 at 21:39
  • @jxh `new T` also dynamically allocates one `T`. It's just that the _new-expression_ does not then have type `T*`. `new T[1]` would have type `T*`, if that's what you're getting at. – aschepler Apr 26 '18 at 21:41
  • Yes, the asker seemed to want to get a `T *`. – jxh Apr 26 '18 at 21:42
  • One more thing to consider, smart pointers: https://tio.run/##fYzLCsIwEEX3/YpBN5VSQTeCfWz6C4ILFQnpqEGT1GSilFI/3Rir4M5hYODeM4c3TXrk3I@F4hdXI@RCWzLIZBn9MolSm7b0oXGcoIIugjAVxBPowFK9XHLtCPIc6CTs@44g2apRBv1APv6j6RftsyiitsEaD0G@2sx3YUMmFIFkQg2SQcgcaSAoPkbJzrh3Slwd5tXm/VTG80n2I2@BVHiH1SejqUGLFN8C03v/5IcLO1qfrtEYbXwapAVPktniBQ – jxh Apr 27 '18 at 01:20