The only things I see that make me suspect oddity are the volatile modifier
The effect is irrespective of volatile, I only added it to be 100% on the safe side (besides calling printf) so nobody could say "optimizing out unused stuff probably gets in the way somehow". That's the reason why I'm using a loop over an array of pointers as well (but it works the same with a single pointer).
The verdict is a different one.
If you thought the output of this program should look somewhat like this:
alloc size 4
alloc size 4
alloc size 4
alloc size 4
alloc size 4
0 @ 0000000000375fa0
0 @ 0000000000375800
0 @ 0000000000375820
0 @ 0000000000375840
0 @ 0000000000375860
delete size 4
delete size 4
delete size 4
delete size 4
delete size 4
exit
... then you are of course right. That is exactly what the output has to look like. A "usual allocation function" with exactly two parameters of which the second one is of type size_t is present, and the allocation is not an array of trivially destructible objects (in which case the single-argument global operator delete is to be used) so operator delete(void*, size_t) noexcept is the correctly chosen overload.
It's what you can easily verify by compiling e.g. with GCC 7.1, too.
However, if you thought that the output might look like this:
alloc size 4
alloc size 4
alloc size 4
alloc size 4
alloc size 4
0 @ 0000000000375fa0
0 @ 0000000000375800
0 @ 0000000000375820
0 @ 0000000000375840
0 @ 0000000000375860
Assert failed: test.cpp, line 9: 0
...then, again, you are of course right. That is exactly what the output may look like. Because, you know, an implementation is allowed to call the single-argument overload instead at its own discretion, and it is a requirement that allocations work the same with either one being called (with or without size). No, I'm not joking.
The entire point of having a sized operator delete in the first place is that you do not need to explicitly store the size of the allocation.
Well,... duh. Except... a program that provides the void* overload should also provide the void*, size_t overload, and a program that provides the void*, size_t overload shall also provide the void* overload. Plus, you are required that it "just works" anyway if you are only being called with void*.
Note the difference in wording. The meaning of shall is that your program is not well-formed if you don't fulfill the precondition (and GCC indeed emits a warning, although it "works fine"). It is well-defined what overload is called in some exceptional cases, but for the general case which version is called is implementation-defined.
But lastly, if you thought that maybe the output could look like:
alloc size 4
alloc size 4
alloc size 4
alloc size 4
alloc size 4
0 @ 000000000049a2e0
0 @ 000000000049a300
0 @ 000000000049a320
0 @ 000000000049a340
0 @ 000000000049a360
exit
... then, again you are of course right. That is exactly what the output may look like. It's what you get if you compile the same source code with clang 5.0, too.
Wait, neither of your overloads being called? What gives? Compiler bug? Sigh... No, if only that was the case...
More specifically, if you look at the generated code (running objdump -dSC, that's the single-object version without loop for brevity), what you get from GCC is this:
0000000000407ed0 <main>:
int main()
{
407ed0: 53 push %rbx
407ed1: 48 83 ec 20 sub $0x20,%rsp
407ed5: e8 86 98 ff ff callq 401760 <__main>
void* operator new ( std::size_t sz ) { printf("alloc size %u\n", sz); return malloc(sz); }
407eda: ba 04 00 00 00 mov $0x4,%edx
407edf: 48 8d 0d 1a 11 00 00 lea 0x111a(%rip),%rcx # 409000 <.rdata>
407ee6: e8 b5 ff ff ff callq 407ea0 <printf(char const*, ...)>
407eeb: b9 04 00 00 00 mov $0x4,%ecx
407ef0: e8 1b fc ff ff callq 407b10 <malloc>
auto x = new int(0);
printf("%d @ %p\n", *x, x);
407ef5: 31 d2 xor %edx,%edx
407ef7: 48 8d 0d 4e 11 00 00 lea 0x114e(%rip),%rcx # 40904c <.rdata+0x4c>
407efe: c7 00 00 00 00 00 movl $0x0,(%rax)
407f04: 49 89 c0 mov %rax,%r8
void* operator new ( std::size_t sz ) { printf("alloc size %u\n", sz); return malloc(sz); }
407f07: 48 89 c3 mov %rax,%rbx
printf("%d @ %p\n", *x, x);
407f0a: e8 91 ff ff ff callq 407ea0 <printf(char const*, ...)>
void operator delete( void* ptr, std::size_t sz ) noexcept { printf("delete size %u\n", sz); free(ptr); }
407f0f: ba 04 00 00 00 mov $0x4,%edx
407f14: 48 8d 0d f4 10 00 00 lea 0x10f4(%rip),%rcx # 40900f <.rdata+0xf>
407f1b: e8 80 ff ff ff callq 407ea0 <printf(char const*, ...)>
407f20: 48 89 d9 mov %rbx,%rcx
407f23: e8 10 fc ff ff callq 407b38 <free> <--- inlined everything, but OK
delete x;
puts("exit");
407f28: 48 8d 0d 26 11 00 00 lea 0x1126(%rip),%rcx # 409055 <.rdata+0x55>
407f2f: e8 cc fb ff ff callq 407b00 <puts>
return 0;
}
407f34: 31 c0 xor %eax,%eax
407f36: 48 83 c4 20 add $0x20,%rsp
407f3a: 5b pop %rbx
407f3b: c3 retq
whereas clang gives you:
00000000004015f0 <main>:
4015f0: 55 push %rbp
4015f1: 48 83 ec 20 sub $0x20,%rsp
4015f5: 48 8d 6c 24 20 lea 0x20(%rsp),%rbp
4015fa: e8 31 02 00 00 callq 401830 <__main>
void* operator new ( std::size_t sz ) { printf("alloc size %u\n", sz); return malloc(sz); }
4015ff: 48 8d 0d fa 79 00 00 lea 0x79fa(%rip),%rcx # 409000 <.rdata>
401606: ba 04 00 00 00 mov $0x4,%edx
40160b: e8 40 00 00 00 callq 401650 <printf(char const*, ...)>
401610: b9 04 00 00 00 mov $0x4,%ecx
401615: e8 c6 65 00 00 callq 407be0 <malloc> <--- So far so good
auto x = new int(0);
40161a: c7 00 00 00 00 00 movl $0x0,(%rax)
printf("%d @ %p\n", *x, x);
401620: 48 8d 0d 24 7a 00 00 lea 0x7a24(%rip),%rcx # 40904b <.rdata+0x4b>
401627: 31 d2 xor %edx,%edx
401629: 49 89 c0 mov %rax,%r8
40162c: e8 1f 00 00 00 callq 401650 <printf(char const*, ...)>
delete x; <--- what about this?
puts("exit");
401631: 48 8d 0d 1c 7a 00 00 lea 0x7a1c(%rip),%rcx # 409054 <.rdata+0x54>
401638: e8 93 65 00 00 callq 407bd0 <puts>
return 0;
40163d: 31 c0 xor %eax,%eax
40163f: 48 83 c4 20 add $0x20,%rsp
401643: 5d pop %rbp
401644: c3 retq <--- wait, that's all???