Существует несколько реализаций функции printf для различных шаблонов. Одним из них является это:
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");
}
и везде говорится, что эта реализация является типобезопасной, в то время как обычный C (с переменными аргументами va_arg) — нет.
Это почему?
Что означает быть безопасным с точки зрения типов и какие преимущества имеет эта реализация по сравнению с реализацией C printf va_arg?
бытие безопасный, или же типобезопасный, означает, что вы можете сказать из глядя на исходный код правильно ли ведет себя ваша программа.
Заявление std::cout << x
всегда правильно, если предположить, что x
имеет четко определенное значение (и не является, скажем, неинициализированным); это то, что вы можете гарантировать, посмотрев на исходный код.
По условию C не безопасно: например, следующий код может быть или не быть правильным, в зависимости от ввода во время выполнения:
int main(int argc, char * argv[])
{
if (argc == 3)
printf(argv[1], argv[2]);
}
Это верно тогда и только тогда, когда первый аргумент является допустимой строкой формата, содержащей ровно одну «%s
».
Другими словами, можно написать правильную программу на C, но невозможно проверить правильность, просто проверив код. printf
функция является одним из таких примеров. В более общем смысле любая функция, которая принимает переменные аргументы, скорее всего небезопасна, как и любая функция, которая приводит указатели на основе значений времени выполнения.
Для всех аргументов, которые вы передаете в вариационную версию шаблона, их типы известны во время компиляции. Это знание сохраняется в функции. Каждый объект затем передается cout
с сильно перегруженным operator<<
, Для каждого переданного типа существует отдельная перегрузка этой функции. Другими словами, если вы передадите int
звонит ostream::operator<<(int)
, если вы передаете двойной, он звонит ostream::operator<<(double)
, Итак, еще раз, тип сохраняется. И каждая из этих функций специализирована для обработки каждого типа соответствующим образом. Это безопасность типа.
С C printf
правда, история другая. Тип не сохраняется внутри функции. Это нужно выяснить, основываясь на содержимом строки формата (которая может быть значением времени выполнения). Функция просто должна предполагать, что была передана правильная строка формата для соответствия типам аргументов. Компилятор не обеспечивает это.
Есть и другой вид безопасности, и это количество аргументов. Если вы передаете слишком мало аргументов в C printf
функция, не достаточно, чтобы соответствовать строке формата, у вас есть неопределенное поведение. Если вы сделаете то же самое с шаблоном variadic, вы получите исключение, которое, хотя и нежелательно, гораздо проще диагностировать.