There are several implementations of variadic templates printf function. One is this:
void printf(const char* s) {
while (*s) {
if (*s == '%' && *++s != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char* s, const T& value, const Args&... args) {
while (*s) {
if (*s == '%' && *++s != '%') {
std::cout << value;
return printf(++s, args...);
}
std::cout << *s++;
}
throw std::runtime_error("extra arguments provided to printf");
}
and everywhere is said that this implementation is type-safe while the normal C (with variadic arguments va_arg) isn't.
Why is that? What does it mean to be type-safe and what advantages has this implementation over a C printf va_arg one?
For all of the arguments you pass to the variadic template version, their types are known at compile time. This knowledge is preserved within the function. Each object is then passed to cout
with the heavily overloaded operator<<
. For each type that is passed, there is a separate overload of this function. In other words, if you pass an int
, it's calling ostream::operator<<(int)
, if you pass a double, it's calling ostream::operator<<(double)
. So again, the type is preserved. And each of those functions is specialized to handle each type in an appropriate way. That's type safety.
With the C printf
though, the story is different. The type is not preserved inside the function. It needs to figure it out based on the contents of the format string (which may be a runtime value). The function just has to assume that a correct format string was passed in to match the argument types. The compiler does not enforce this.
There is another kind of safety too, and that is in the number of arguments. If you pass too few arguments to the C printf
function, not enough to match the format string, you have undefined behavior. If you do the same thing with the variadic template, you get an exception, which, while not desirable, is a much easier problem to diagnose.
Being safe, or type-safe, means that you can tell from looking at the source code whether your program behaves correctly.
The statement std::cout << x
is always correct, assuming that x
has a well-defined value (and isn't, say, uninitialized); this is something you can guarantee from looking at the source code.
By constrast, C is not safe: For example, the following code may or may not be correct, depending on the runtime input:
int main(int argc, char * argv[])
{
if (argc == 3)
printf(argv[1], argv[2]);
}
This is correct if and only if the first argument is a valid format string containing precisely one "%s
".
In other words, it is possible to write a correct C program, but it's impossible to reason about the correctness just by inspecting the code. The printf
function is one such example. More generally, any function that accepts variable arguments is most likely unsafe, as is any function that casts pointers based on runtime values.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With