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::formatorfmt - 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.