Parameter "size" of member operator new[] increases if class has destructor/delete[]
Asked Answered
C

1

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)
Cryptomeria answered 20/8, 2017 at 11:35 Comment(7)
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.Bloxberg
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.Newtonnext
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.Diaphane
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.Desolation
The added space is 8 bytes shared between 10 objects so its not a per object cost.Wages
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.Jurel
Possible duplicate of Operator new[] does not receive extra bytesSterculiaceous
J
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 ;)

Jurel answered 20/8, 2017 at 12:53 Comment(5)
There is nothing hidden in assembly.Cryptomeria
It looks like 'new' stores the size of the block, just like 'malloc'. So, actually the size is stored twice, once for malloc and once for new - looks very inefficient.Endstopped
@Endstopped It looks like 'new' stores the size of the objects, while 'malloc' stores the size of the bytes.Cryptomeria
I want to know how you get the C++ version of the assembly.Cryptomeria
@Cryptomeria I wrote it myself from the assembly - it is just to help those that don't read assembly that well.Jurel

© 2022 - 2024 — McMap. All rights reserved.