8

4 classes in the following codes: A, B, C and D.

They all have a member operator new[].

Besides,

  • B has a constructor;
  • C has a destructor;
  • D has a member operator delete[].

The Parameter size of member operator new[] and the sizeof of the 4 classes are output:

new[] A 40
new[] B 40
new[] C 48
new[] D 48
sizeof(A) 4
sizeof(B) 4
sizeof(C) 4
sizeof(D) 4

What's the reason for the differences of size?

Codes(ugly I know):

#include <iostream>
using namespace std;

class A {
    int i;
public:
    static void* operator new[](std::size_t size) throw(std::bad_alloc) {
        cout << "new[] A " << size << endl;
        return malloc(size);
    }
};

class B {
    int i;
public:
    static void* operator new[](std::size_t size) throw(std::bad_alloc) {
        cout << "new[] B " << size << endl;
        return malloc(size);
    }
    B() {}
};


class C {
    int i;
public:
    static void* operator new[](std::size_t size) throw(std::bad_alloc) {
        cout << "new[] C " << size << endl;
        return malloc(size);
    }
    ~C() {}
};

class D {
    int i;
public:
    static void* operator new[](std::size_t size) throw(std::bad_alloc) {
        cout << "new[] D " << size << endl;
        return malloc(size);
    }
    static void operator delete[](void* p, std::size_t size) {
        free(p);
    }
};

int main() {
    A* a = new A[10];
    B* b = new B[10];
    C* c = new C[10];
    D* d = new D[10];
    cout << "sizeof(A) " << sizeof(A) << endl;
    cout << "sizeof(B) " << sizeof(B) << endl;
    cout << "sizeof(C) " << sizeof(C) << endl;
    cout << "sizeof(D) " << sizeof(D) << endl;
}

About OS and compiler:

Compiling: same results for clang++ and g++

clang++ test.cpp -o test -std=c++11
g++     test.cpp -o test -std=c++11

OS: Linux Mint 18.2 Cinnamon 64-bit

Compilers:

clang++ -v

clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/4.9.3
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/5.4.0
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/6.0.0
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.9
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.9.3
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/5.4.0
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6.0.0
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/5.4.0
Candidate multilib: .;@m64
Selected multilib: .;@m64
Found CUDA installation: /usr/local/cuda

g++ -v

Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.4' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4)
underscore_d
  • 6,309
  • 3
  • 38
  • 64
chaosink
  • 1,329
  • 13
  • 27
  • Since this is not something specified by the standard, would you please mention what platform (OS, 32/64-bit) and compiler you are using? Also, whether optimization is enabled when you compile. – John Zwinck Aug 20 '17 at 11:37
  • Am I the only one? `prog.cc:7:51: error: ISO C++1z does not allow dynamic exception specifications` with GCC 8. @JohnZwinck (s)he mentions 64 bit. – gsamaras Aug 20 '17 at 11:48
  • Not using clang, but another compiler. In cases A and B the default destructor does nothing and so the compiler doesn't care to call it. In case C we have a user defined constructor which is called, and so the compiler has to store how many times to call it. Might need some extra bytes for that. Similar in case D we somehow need to get the `size` parameter to `delete[]`. But just speculations. Also, if the class members were something more complicated than an `int`, the results could have been different. – Bo Persson Aug 20 '17 at 11:53
  • All this is in the realms of implementation defined behaviour, so can only guess. It MAY be a result of book-keeping and optimisation by the compiler - for example, allocating additional memory for book-keeping when there is a non-default destructor since, with a `delete []` expression, the destructor must be called for every allocated object. Similar story if an `operator delete[](void &, size_t)` is provided, since it is necessary to pass the size from the new expression somehow. – Peter Aug 20 '17 at 12:01
  • 1
    The added space is 8 bytes shared between 10 objects so its not a per object cost. – Surt Aug 20 '17 at 12:03
  • These extra bytes are used to store the allocated size - For `A` and `B`, you don't care because there is nothing to destruct (you don't have to know how many elements you'll have to destruct), and you don't need to keep the information to call the built-in `delete[]` (the compiler does not need it). For `C`, the compiler needs to know how much elements must be destroyed when calling `delete[]`, and for `D` it needs to know what to pass to `D::operator delete[]`. You can verify these easily by looking at the generated assembly. – Holt Aug 20 '17 at 12:35
  • 3
    Possible duplicate of [Operator new\[\] does not receive extra bytes](https://stackoverflow.com/questions/13746517/operator-new-does-not-receive-extra-bytes) – Raymond Chen Aug 20 '17 at 13:41

1 Answers1

9

These extra 8 bytes are used to store information regarding what has been allocated in order to destruct objects correctly (the program needs to know how many objects need to be destroyed) and to call T::operator delete[] with the correct second parameter. According to the generated assembly (see the end of this answer), the value stored is the number of elements (here 10).

Basically:

  • for A and B, the destructor is a no-op, so there is no need to know how many elements must be destroyed, and you don't have a user-defined delete[], so the compiler will use the default one, which apparently does not care about the second parameter;

  • for C, the destructor is used-defined, so it must be called (I don't know why this is not optimized... ), so the program needs to know how many objects will be destroyed;

  • for D, you have a user-defined D::operator delete[], so the program must remember the allocated size in order to send it to D::operator delete[] when necessary.

If you replace the int attribute with a type that has a non-trivial destructor (e.g. std::vector<int>), you will notice these 8 bytes for both A and B.

You can look at the generated assembly for C (g++ 7.2, no optimization):

; C *c = new C[10];
  call C::operator new[](unsigned long)
  mov QWORD PTR [rax], 10   ; store "10" (allocated objects)
  add rax, 8                ; increase pointer by 8
  mov QWORD PTR [rbp-24], rax

; delete[] c;
  cmp QWORD PTR [rbp-24], 0
  je .L5
  mov rax, QWORD PTR [rbp-24] ; this is c
  sub rax, 8
  mov rax, QWORD PTR [rax] ; retrieve the number of objects
  lea rdx, [0+rax*4]       ; retrieve the associated size (* sizeof(C))
  mov rax, QWORD PTR [rbp-24]
  lea rbx, [rdx+rax]
.L7:
  cmp rbx, QWORD PTR [rbp-24] ; loops to destruct allocated objects
  je .L6
  sub rbx, 4
  mov rdi, rbx
  call C::~C()
  jmp .L7
.L6:
  mov rax, QWORD PTR [rbp-24]
  sub rax, 8
  mov rax, QWORD PTR [rax] ; retrieve the number of allocated objects
  add rax, 2               ; add 2 = 8 bytes / sizeof(C)
  lea rdx, [0+rax*4]       ; number of allocated bytes
  mov rax, QWORD PTR [rbp-24]
  sub rax, 8
  mov rsi, rdx
  mov rdi, rax
  call operator delete[](void*, unsigned long)

If you are not familiar with assembly, here is an arranged C++ version of what happens under the hood:

// C *c = new C[10];
char *c_ = (char*)malloc(10 * sizeof(C) + sizeof(std::size_t)); // inside C::operator new[]
*reinterpret_cast<std::size_t*>(c_) = 10; // stores the number of allocated objects
C *c = (C*)(c_ + sizeof(std::size_t));    // retrieve the "correct" pointer

// delete[] c; -- destruction of the allocated objects
char *c_ = (char*)c;
c_ -= sizeof(std::size_t); // retrieve the original pointer
std::size_t n =            // retrieve the number of allocated objects
    *reinterpret_cast<std::size_t*>(c_); 
n = n * sizeof(C);         // = n * 4, retrieve the allocated size
c_ = (char*)c + n;         // retrieve the "end" pointer
while (c_ != (char*)c) {
    c_ -= sizeof(C);                  // next object
    (*reinterpret_cast<C*>(c_)).~C(); // destruct the object
}

// delete[] c; -- freeing of the memory
char *c_ = (char*)c;
c_ -= sizeof(std::size_t);
std::size_t n = 
    *reinterpret_cast<std::size_t*>(c_); // retrieve the number of allocated objects
n = n * sizeof(C) + sizeof(std::size_t); // note: compiler does funky computation instead of 
                                         // this, but I found this clearer
::operator delete[](c_, n);

Now you're happy to know that the compiler does all of this for you ;)

Holt
  • 36,600
  • 7
  • 92
  • 139