Tail-call optimization in C is relatively recent (2025)

Tail-call optimization in C and related languages is emerging from a niche compiler trick into something developers want to rely on for correctness, not just speed. Commenters contrast languages where tail calls are guaranteed by the specification (like Scheme, F#, or JavaScriptCore’s implementation of JS) with C, C++, Rust, and C# where TCO is usually treated as an optional optimization, prompting proposals such as `[[musttail]]` in C++ and a `become` keyword in Rust to make failed TCO a compile-time error. The thread also delves into how calling conventions, variadic functions, destructors/RAII, and tooling constraints make proper tail calls hard to implement, and why they’re crucial for interpreters, state machines, and writing recursive code without risking stack overflows.

Scope and guarantees of TCO

  • Strong sentiment that treating tail-call optimization (TCO) as “just an optimization” is problematic, because correctness (avoiding stack overflow, enabling certain styles) can depend on it.
  • Contrast between languages where TCO is mandated by the spec (e.g., Scheme) and C/C++/JS where it is optional and implementation‑dependent.
  • Several people argue that without a language‑level guarantee or a “must tail” mechanism, relying on TCO is unsafe.

Attributes, keywords, and language support

  • Modern C/C++ compilers (GCC/Clang/MSVC) expose [[...::musttail]] attributes: they try hard to TCO and emit a compile error if they cannot.
  • Proposed Rust become keyword: explicitly requests TCO, drops all local values first so the call is in true tail position, and makes failure to do TCO a compile error.
  • Comparisons to Scala’s @tailrec (limited to self-recursion) and F#’s IL tail. instructions on .NET; C# does not reliably emit them.

C standardization and history

  • C itself still has no guaranteed TCO; current support is compiler‑specific extensions.
  • A technical specification (TS) for C tail calls and for defer is being drafted; even once standardized, adoption will be slow and implementations can be conforming yet reject the syntax.
  • Discussion notes older GCC limitations (e.g., indirect calls, variadics) and that usable, general TCO in GCC is relatively recent compared to its lifetime.
  • MSVC historically had weak C and TCO support, improved somewhat in recent years but still viewed skeptically by some for C.

Technical hurdles: calling conventions and stack management

  • Variadic functions and pre‑ANSI C calling conventions complicate proper tail calls, since only the caller knows argument size and must clean up the stack.
  • Caller‑ vs callee‑cleanup conventions and cross‑module calls make generic, guaranteed TCO hard; function pointers and exported symbols further constrain compilers.
  • Debate over how much calling convention matters when the compiler controls both caller and callee; others point out separate compilation and ABI stability constraints.

Practical patterns and style debates

  • TCO is important for interpreters, state machines, continuation‑passing style, and mutually recursive function sets, not just recursive loops like factorial.
  • Some prefer explicit loops in C; others find tail‑recursive formulations clearer and argue TCO enables nicer abstractions, especially in more functional styles.
  • Manual “TCO” with goto is shown but discouraged as error‑prone compared to true recursion plus compiler support.