как победить 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; то так просто не получится.
Пыхх... Я думал что ты понимаешь что предлагалось сделать...
Ладушки, опишу чуть подробнее.
В стек для спринтф() в любом случае ляжет указатель.
Либо на строку - тогда указатель на сегмент данных, либо на объект - тогда указатель на сегмент кода.
Соответственно, для ес-процент проверяешь куда указатель, объектный кастишь к типу объекта и вызываешь метод конверсии.
Метод вернет указатель на реальную строку и уже через этот указатель отдаешь ес-проценту.
Ну и не помешает в лог капнуть что такое имело место и где именнo...
В стек для спринтф() в любом случае ляжет указатель
ничего подобного! Ляжет то что было подставлено в аргументе. Может даже класс любого размера.
кастишь к типу объекта
тип узнать невозможно. я уже говорил: в стеке лежат какието числа, которые могут быть чем угодно: указателем, интом, доублом, или телом любого объекта любого типа, даже такого у которого никаких строк и в помине нет
Ляжет то что было подставлено в аргументе.
-----
Разумеется - именно то, что, по факту, прописано в параметрах.
Ожидаемое, однако, есть указатель на начало нуль-терминальной строки в сегменте данных.
в стеке лежат какието числа, которые могут быть чем угодно
-----
Ээээ... там лежат "числа", которые ты можешь интерпретировать как тебе нужно. Для спецификатора
'%s'
ты должен ожидать "указатель-на-строку" (в сегменте данных).
Если данные не соответствуют ожидаемым, т.е. имеется "указатель-в-другой-сегмент", то с его можно
интерпретировать как
void*
и кастить к нужному типу. Ну или смотреть что по факту компилятор туда складывает.
То. что у тебя может быть набор параметров никак не соответствующий заданному формату - это понятно, но в Сях с эллипсисом оно таково изначально и не решаемо средствами Сей.
Рекомендация относится к случаю когда параметр на месте, но не соответствует нужному типу - отследить и пофиксить в ран-тайме.
или телом любого объекта любого типа
-----
Тебе сложно скастить к ссылке на объект с инициированием адресом в стеке?
Разумеется - именно то, что, по факту, прописано в параметрах. Ожидаемое, однако, есть указатель на начало нуль-терминальной строки в сегменте данных.ожидаемо, но проверить что лежащее в стеке является именно указателем - невозможно.
там лежат "числа", которые ты можешь интерпретировать как тебе нужно.мне допустим нужен указательна стринг. Естественно я буду его так и интерпретировать (что делает sprintf и без меня). Но там лежит какой-то INT напр, который при касте к указателю даже указывает на сегмент данных, но естественно либо в мусор, либо в середину какой-нибудь посторонней строки, либо туда где находится 0. Во втором и третьем случаях вообще понять что ошибка - невозможно. И шо теперь делать?
интерпретировать как void* и кастить к нужному типу.к какому именно типу скастить? Ведь нету никакой информации об этом.
Тебе сложно скастить к ссылке на объект с инициированием адресом в стеке?да скастить то не сложно. Сложно понять к чему кастить (а точнее невозможно понять, о чем уже говорилось)
Но там лежит какой-то INT напр
-----
Ты на что изначально жаловался? На то, что местами вместо чар* пихается стд::Стринг. Откуда у тебя вдруг появляется инт Я как-то не понимаю.
к какому именно типу скастить?
-----
К тому который ожидается - стд::стринг. В стеке этой информации разумеется нету, но именно эту часть ты изложил изначально.
Да, еще одна мысль по детектированию проблемы пришла - сравнить глубину стека с ожидаемым количество-размером фактически переданных параметров - смещение до адреса возврата в стеке.
Во втором и третьем случаях вообще понять что ошибка - невозможно. И шо теперь делать?
-----
То же что всегда в Сях - фаулт...
Т.е. Примерно тоже самое когда потрешь адрес возврата в стеке...