Variadic parameter and additional default value not compiling

Summary

This postmortem analyzes a C++ template bug where a variadic parameter pack becomes unusable because a defaulted trailing parameter (std::source_location) prevents correct overload resolution. The result is a confusing compile‑time error when calling _LOG() with multiple arguments.

Root Cause

The root cause is the placement of the default parameter after a variadic pack:

  • A function template of the form
    f(T... args, U defaulted = value)
    is not legal in C++.
  • The compiler cannot determine where the variadic pack ends and where the defaulted parameter begins.
  • This leads to no viable overload when calling _LOG() with multiple arguments.

In short: variadic packs must be the last parameters in a function template.

Why This Happens in Real Systems

Real production systems hit this issue because:

  • Variadic templates are often used to wrap logging, formatting, or forwarding.
  • Engineers try to add metadata parameters (e.g., source_location, timestamps, context IDs).
  • They attempt to place these metadata parameters after the variadic pack.
  • C++ template rules do not allow this, causing subtle compile‑time failures.

Real-World Impact

This type of bug causes:

  • Broken logging pipelines where logs silently fail to compile.
  • Hard‑to‑diagnose template errors that confuse even experienced developers.
  • Inconsistent behavior when some calls compile and others do not.
  • Undefined behavior when developers “fix” the issue by removing arguments.

Example or Code (if necessary and relevant)

A correct pattern is to move the metadata parameter into a separate overload or wrap it in a struct so the variadic pack remains last.

Here is a valid approach using a wrapper overload:

template
struct LOGGER {
    template
    auto _LOG(const char* formatString,
              const std::source_location& sourceLocation,
              LogArguments... logArguments) -> void
    {
        char formattedLog[BUFFER_SIZE] {};
        snprintf(formattedLog, sizeof(formattedLog), formatString, logArguments...);
        LogControllerContainer::dispatchLog(
            static_cast(this)->m_member,
            formattedLog,
            sourceLocation
        );
    }

    template
    auto _LOG(const char* formatString, LogArguments... logArguments) -> void
    {
        _LOG(formatString, std::source_location::current(), logArguments...);
    }
};

This preserves:

  • Variadic pack at the end
  • Default source_location
  • Clean call sites

How Senior Engineers Fix It

Experienced engineers typically apply one of these patterns:

  • Overload forwarding (as shown above)
  • Metadata struct bundling so the variadic pack stays last
  • Explicit wrapper macros for logging
  • Compile‑time format checking using std::format or fmt
  • Avoiding variadic C‑style formatting entirely

The key principle: never place a defaulted parameter after a variadic pack.

Why Juniors Miss It

Junior engineers often miss this because:

  • Variadic templates look like C-style varargs, but behave very differently.
  • They assume default parameters work anywhere.
  • Compiler diagnostics for template deduction failures are notoriously cryptic.
  • They do not yet recognize the rule:
    “Variadic template parameters must be last.”

This is a classic example of C++ template mechanics being far stricter than they appear.

Leave a Comment