As others have pointed out, this is undefined behavior, so all bets are off about what may in principle happen. But assuming that you’re on an x86 machine, there’s a plausible explanation as to why you’re seeing this.
On x86, the g++ compiler doesn’t always pass arguments by pushing them onto the stack. Instead, it stashes the first few arguments into registers. If we disassemble the f function, notice that the first few instructions move the arguments out of registers and explicitly onto the stack:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi # <--- Here
mov DWORD PTR [rbp-8], esi # <--- Here
# (many lines skipped)
Similarly, notice how the call is generated in main. The arguments are placed into those registers:
mov rax, QWORD PTR [rbp-8]
mov edx, 30 # <--- Here
mov esi, 20 # <--- Here
mov edi, 10 # <--- Here
call rax
Since the entire register is being used to hold the arguments, the size of the arguments isn’t relevant here.
Moreover, because these arguments are being passed via registers, there’s no concern about resizing the stack in an incorrect way. Some calling conventions (cdecl) leave the caller to do cleanup, while others (stdcall) ask the callee to do cleanup. However, neither really matters here, because the stack isn’t touched.