как победить sprintf ?
в проэкте встречается много sprintf который, как известно, принимает любые аргументы и в любом количестве там где три точки: sprintf(char* buffer, const char* format, ...);
Беда в том что у одного из аргументов (который в троеточии) поменялся тип. Например, раньше был const char*, а стал std::string.
Студия 2015 дает хоть предупреждение в этом случаее, а вот Студия 2013 почему-то не даёт! Опции проэктов сравнивал - аналогичные. Программа просто вылетает при выполнении. Вот думаю как гарантированно найти все такие места?
Вариант конечно есть, но трудоёмкий: не использовать sprintf вообще, а поиском/заменой заменить на MySprintf, которых сделать кучу на все используемые в проекте случаи, без трёх точек.
Но может есть проще вариант?
решил таки задачку более менее приемлемым способом:
- использование своей функции (меняем поиском/заменой) + несколько темплейтов
теперь компилятор ругается, если в список (который раньше был троеточием) попадает тип, что я не указал.
template<class T> void check_t(T) { wrong type!!!; } // здесь ругается компайлер
template<> inline void check_t(const char*) {}
template<> inline void check_t(char*) {}
template<> inline void check_t(int) {}
template<> inline void check_t(long) {}
template<> inline void check_t(unsigned long) {}
template<> inline void check_t(unsigned long long) {}
template<> inline void check_t(unsigned) {}
template<> inline void check_t(double) {}
template<> inline void check_t(char) {}
template<> inline void check_t(unsigned char) {}
template<class T>
void sprintf_my(char* buf, size_t size, const char* format, T t)
{
check_t(t);
sprintf_s(buf, size, format, t);
}
..... до шести аргументов бывает .....
template<class T, class T2, class T3, class T4, class T5, class T6>
void sprintf_my(char* buf, size_t size, const char* format, T t, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6)
{
check_t(t);
check_t(t2);
check_t(t3);
check_t(t4);
check_t(t5);
check_t(t6);
sprintf_s(buf, size, format, t, t2, t3, t4, t5, t6);
}
да нет, как раз недостаток функций с тремя точками именно есть - отсутствие контроля типа. Т.е. туда передать можно всё что угодно.
поэтому
sprintf(buf, "%s", std::string());
прекрасно компилируется, но приводит к вылету при выполнении.
Кстати в некоторых случаях подобный код работает без вылета. А точнее такой:
sprintf(buf, "%s", CString());
и работает он только потому что внутри себя CString самыми первыми данными (а в релизе других и нет) располагает сам указатель на стринг. Ну и еще одно условие успешной работы - этот CString должен быть последним в списке, ибо после него имплементации sprintf-а уже невозможно вычислить адрес следующего параметра (ведь %s для sprintf-а означает что следующий будет через 4 байта, а не через sizeof(CString), о размере которого он понятия не имеет).
Самое правильное, заменить все переменные std::string s в sprintf на s.c_str()
это верно, но в достаточно большом проекте это нереально. А вот с описанным выше способом - реально, причем сам компилятор покажет где это нужно подправить.
Кстати для чистоты описанный выше способ можно заключить в #ifdef _DEBUG. Хотя по идее в релизе компайлер и сам оптимизирует инлайн функции, а пустые как check_t вообще выбросит.
Еще один вариант - помнить в каком порядке линкуются обьектники в Сях.
Сначала идут те которые компилируются в проекте, затем те которые в указанных либах.
Т,е, взяв имплементацию спринтf() из какой-нибудь библиотеки и добавив контроль типов получишь что надо.
У тебя вроде микс - С/Срр - но там отличий нет.
взяв имплементацию спринтf() из какой-нибудь библиотеки и добавив контроль типов получишь что надоа как добавить контроль типов?
Решение выше можно слегка улучшить. А именно не делать новое имя sprintf_my, а использовать namespace:
namespace XX
{
template<class T>
void sprintf_s(char* buf, size_t size, const char* format, T t)
{
check_t(t);
::sprintf_s(buf, size, format, t);
}
}
using namespace XX;
это позволит обойтись без переименования каждого вызова в уже существующем коде.
(Правда и недостаток есть: можно забыть сделать #include "my_sprintf.h")
всё равно толку не будет.
В случае std::string, там вообще никаких указателей не будет, т.к. лежать в стеке будет целый экземпляр класса, а что там за данные у него внутри - кто знает? И к тому же std::string это лишь к примеру было, могут быть и другие классы.
Но самый главный аргумент, что бестолку, это то, что ошибку надо найти на этапе компиляции, а не выполнения.
лежать в стеке будет целый экземпляр класса
-----
Ну так тебе этого достаточно - не в сегменте данных - класс - надо разбираться какой и где реальные данные.
ошибку надо найти на этапе компиляции
-----
Да, это будет удобнее. Хотя и не всегда возмозhно.
Ну так тебе этого достаточно - не в сегменте данных - класс - надо разбираться какой и где реальные данные.
так всё равно нет возможности узнать что там находится. В стеке будут какие-то числа. Которые могут быть любыми. Там может быть даже "Hello World" прямым текстом без указателей. Числа могут быть похожими и на указатели (ими не являясь) хоть на данные, хоть на код.
В стеке будут какие-то числа. Которые могут быть любыми.
-----
В Сях - не совсем любые.
В Сях для спецификатора %s на вершине стека (в зависимости от реализации - с порядковым смещением) должен быть указатель на строку. Т.е. адрес в сегменте данных.
прямым текстом без указателей.
-----
Мы таки говорим об Сях? Или об чем-то другом?
В тех реализациях которые Я помню - там ожидается указатель.
В Сях для спецификатора %s на вершине стека (в зависимости от реализации - с порядковым смещением) должен быть указатель на строку.компилятор для sprintf никак это не контролирует. Там может даже ничего не оказаться вообще (т.е. мусор), а может и - любой тип.
Я помню - там ожидается указатель.sprintf то ожидает, но в том то и проблема что программист туда загнал не то что sprintf ожидает, и задача именно в том чтобы это распознать до запуска программы на выполнение.
никак это не контролирует.
-----
Ну разве Я с этим где-то спорю? Разумеется, количество и типы параметров при передаче по элипсису никак не контролируется.
проблема что программист туда загнал не то
-----
Именно это Я тебе и предлагаю отследить. В ран-тайме. Бо, не всегда можно сделать на уровне компиляции. Про void** помнишь?
Именно это Я тебе и предлагаю отследить. В ран-таймев солюшине 60 проэктов и почти 8 тысяч sprintf-ов. Да мне жизни не хватит так оттестить, чтоб заставить всех их по разу выполниться.
Кстати я уже заимплементил, попутно заменяя sfrintf на sprint_s везде где возможно (точнее компайлер сам мне показал все места) и штук 5 баг с неверным типом в трех точках выловил. Конечно баги древние, и возможно в мертвом коде. Но зато теперь не нужно бояться за новую багу, если где тип поменяется.
попутно заменяя sfrintf на sprint_s везде где возможно
-----
Ничего не понял.
Есть стандартная библиотека - содержит спринтф().
Написанный код, любой код в проекте, имеющий реализацию спринтф() будет использоваться вместо библиотечного.
Зачем нужна спринт_с()?
Да мне жизни не хватит так оттестить
-----
Потому и предлагается обработка по месту.
Зачем нужна спринт_с()?sprintf_s тоже библиотечная, но безопаснее чем sprintf, т.к. проверяет размер буфера.
И если раньше был такой вызов
sprintf(buf, format, a, b, c)
где буфер был объявлен как char buf[123];
то можно прото заменить на
sprintf_s(buf, format, a, b, c)
и всё скомпилируется.
Но вот если буфер был объявлен как char* buf; то так просто не получится.