1. 11

  2. 10

    My favourite proposed C23 feature is Tail Call Elimination.

    This will use the syntax return goto f(x) to mark a tail call.

    1. 5

      That features seems a long way away from being implementable.

      Back in the ’80s, one of the big questions in calling convention design was who is responsible cleaning up the argument frame: the caller or callee. Pascal generally chose the callee, C implementations chose the caller. This is why you see the Pascal calling convention in a bunch of ’80s and ‘90s C code. If it’s the callee, then you can fold the stack-pointer adjust from popping the top stack frame and the stack-pointer adjust from cleaning up the arguments together, so if you can get away with it it’s preferable.

      The reason that C compilers generally went with caller-cleanup is related to variadic functions. In a C variadic function the caller knows the size of the argument frame, the caller does not. This means that, for variadic calls, caller-cleans-up is the only possible choice. With ANSI C (C89), you could have a different calling convention for variadic and non-variadic functions (prior to that it wasn’t possible: function prototypes were optional and variadics were implemented by just taking the address of the last formal parameter and walking up the stack - C89 added enough to permit calling conventions that passed some arguments in registers). In practice, this is a bad idea for performance (the argument juggling when printf calls vprintf, for example, would be exciting) and a lot of C code has now been written on the assumption that you can use a variadic function pointer to call a non-variadic function. This is technically UB, but it’s the kind of UB that if you do anything other than what the original C compiler did then you break everything and it’s your fault.

      This means that, if a C function wants to tail call a C function that takes the same number of arguments then it can do so by replacing them in the argument slots. If a C function that takes only register arguments wants to call another C function that takes only register arguments, that’s also fine. If a C function that takes register arguments wants to tail-call a function that takes stack arguments, then that isn’t possible.

      The Clang `[[clang::musttail]`` attribute restricts itself to only permitting functions that take the same number / types of arguments as the caller. This sounds fine but it’s actually not sound. For example, consider this example:

      void fill(int*, int);
      int manyArgs(int, int, int, int, int, int, int, int, int, int); // Keep adding parameters until it doesn't fit in registers
      static int manyArgs2(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) // Keep adding parameters until it doesn't fit in registers
         // Do stuff to some of the arguments here
         [[clang::musttail]] return manyArgs(a,b,c,d,e,f,g,h,i,j); // Fine, same arguments as caller.
      int outer(int x)
        int buffer[10];
        fill(buffer, x);
        return manyArgs2(buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7], buffer[8], buffer[9]);

      Now what happens? The compiler sees manyArgs2 has a single caller and inlines it. Now there’s a guaranteed tail call of a function that passes on-stack arguments being called from a function that doesn’t pass on-stack arguments. Clang generates code without the tail call but doesn’t warn.

      If this made the cut for C23 then it just highlights the fact that no one who implements C-family languages cares enough about C anymore to engage with WG14 - even an aggressively restricted version of this feature is almost impossible to implement correctly. I honestly have no idea why anyone who writes code for anything other than 8-bit microcontrollers would write C instead of C++ these days.

      1. 1

        I don’t agree with the analysis. From my perspective, the C++ compiler is fulfilling the contract, which is that only one stack frame is used for calling both manyArgs and manyArgs2 from outer. When manyArgs2 returns, one stack frame is popped and it returns back to outer. That is what the compiler is being asked to guarantee via the ‘musttail’ pragma, and that’s what happens. Sure, the optimizer is rearranging the code, but the point of a tail call guarantee is to put a bound on stack consumption, and the generated code is not exceeding the budget.

      2. 2

        Oh, nice! Clang 13 has a nonstandard __attribute__((musttail)) for that. I haven’t had a chance to try it out because for some reason the Clang used in Xcode doesn’t support it (despite identifying as Clang 13.)

        1. 2

          Apple Clang version has nothing to do with vanilla Clang version on which it is based. In fact, there is no guarantee it’s based on any particular release of vanilla Clang. For example, Apple Clang 13.0.0 is based on something in the 12.0.0 development cycle (and yes, the .0.0 part in 13.0.0 is important – Apple is known to have changed the upstream to a new major version for its patch release).

          1. 1

            Hm, I know that’s been true in the past … maybe the coincidence of both reaching version 13 fooled me into thinking that -v was now reporting the real Clang version.

          2. 1

            __attribute__((musttail)) has very restrictive requirements, compared to the proposed C23 feature. Although the C23 feature could become more restrictive in committee.

            The target function must have the same number of arguments as the caller. The types of the return value and all arguments must be similar according to C++ rules (differing only in cv qualifiers or array size), including the implicit “this” argument, if any. Any variables in scope, including all arguments to the function and the return value must be trivially destructible. The calling convention of the caller and callee must match, and they must not be variadic functions or have old style K&R C function declarations.