Я работаю с инструментом UNIX с открытым исходным кодом, который реализован на C++, и мне нужно изменить код, чтобы заставить его делать то, что я хочу. Я хотел бы внести как можно меньшие изменения в надежде, что мой патч будет принят в апстриме. Предпочтительны решения, реализуемые на стандартном C++ и не создающие дополнительных внешних зависимостей.
Вот моя проблема. У меня есть класс C++ - назовем его «A» - который в настоящее время использует fprintf () для печати своих сильно отформатированных структур данных в указатель файла. В своей функции печати он также рекурсивно вызывает идентично определенные функции печати нескольких классов-членов (например, «B»). Существует еще один класс C, у которого есть член std :: string "foo", который должен быть установлен для результатов print () экземпляра A. Думайте об этом как о функции-члене to_str () для A.
В псевдокоде:
class A {
public:
...
void print(FILE* f);
B b;
...
};
...
void A::print(FILE *f)
{
std::string s = "stuff";
fprintf(f, "some %s", s);
b.print(f);
}
class C {
...
std::string foo;
bool set_foo(std::str);
...
}
...
A a = new A();
C c = new C();
...
// wish i knew how to write A's to_str()
c.set_foo(a.to_str());
Я должен упомянуть, что C довольно стабилен, но A и B (и остальные иждивенцы A) находятся в состоянии изменения, поэтому чем меньше изменений кода необходимо, тем лучше. Текущий интерфейс печати (FILE * F) также необходимо сохранить. Я рассмотрел несколько подходов к реализации A :: to_str (), каждый из которых имеет свои преимущества и недостатки:
Измените вызовы с fprintf () на sprintf ()
Попробуйте поймать результаты a.print () в строковом потоке
Используйте строку Boost библиотека форматов
printf (format_str, args) -> cout << boost :: format (format_str)% arg1% arg2% и т. д.
Используйте Qt QString :: asprintf ()
Итак, исчерпал ли я все возможные варианты? Если да, то какой, по-вашему, я лучше всего ставлю? Если нет, то что я упустил?
Спасибо.
Это о сериализации? Или собственно печать? Если первое, рассмотрите также boost :: serialization. Все дело в «рекурсивной» сериализации объектов и подобъектов.
Речь идет о правильной печати. C.foo - это часть данных, которая в конечном итоге отображается пользователю (в основном) как есть. Если бы это был мой код, я бы потерял бессмыслицу print (FILE *), которая является слишком строгой.
Я использую № 3: библиотеку форматов строк Boost, но должен признать, что у меня никогда не было проблем с различиями в спецификациях формата.
Для меня работает как шарм - и внешние зависимости могут быть хуже (очень стабильная библиотека)
Отредактировано: добавлен пример использования boost :: format вместо printf:
sprintf(buffer, "This is a string with some %s and %d numbers", "strings", 42);
было бы что-то вроде этого с библиотекой boost :: format:
string = boost::str(boost::format("This is a string with some %s and %d numbers") %"strings" %42);
Надеюсь, это поможет прояснить использование boost :: format.
Я использовал boost :: format в качестве замены sprintf / printf в 4 или 5 приложениях (запись форматированных строк в файлы или пользовательский вывод в файлы журналов) и никогда не испытывал проблем с различиями в форматах. Могут быть некоторые (более или менее неясные) спецификаторы формата, которые отличаются от других, но у меня никогда не было проблем.
Напротив, у меня были некоторые спецификации формата, которые я не мог использовать с потоками (насколько я помню)
Спасибо за разъяснения по использованию boost :: format. Заманчиво, учитывая, что этот проект уже зависит от другой библиотеки boost, но я не думаю, что что-то лучше printf, которое просто работает с std :: string, как, кажется, делает Локи.
Я попробовал SafeFormat от Loki, но оказалось, что он просто меняет 5s ускорения на () s. Положительным моментом является то, что мой код работал после того, как я принял boost :: format :-)
Приятно слышать, что у вас работает boost :: format - никогда не пробовал метод Локи.
Вы можете использовать std :: string и iostreams с форматированием, например вызов setw () и другие в iomanip.
Спасибо, что сообщили мне об iomanip. Это, безусловно, выглядит полезным, но я хочу попытаться максимально сохранить многочисленные существующие форматные строки.
К сожалению, iostream и его манипуляторы - очень плохая замена строкам формата printf ().
Вот идиома, которая мне нравится, когда я делаю функциональность идентичной sprintf, но возвращая std :: string и невосприимчив к проблемам переполнения буфера. Этот код является частью проекта с открытым исходным кодом, который я пишу (лицензия BSD), поэтому каждый может использовать его по своему усмотрению.
#include <string>
#include <cstdarg>
#include <vector>
#include <string>
std::string
format (const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
std::string buf = vformat (fmt, ap);
va_end (ap);
return buf;
}
std::string
vformat (const char *fmt, va_list ap)
{
// Allocate a buffer on the stack that's big enough for us almost
// all the time.
size_t size = 1024;
char buf[size];
// Try to vsnprintf into our buffer.
va_list apcopy;
va_copy (apcopy, ap);
int needed = vsnprintf (&buf[0], size, fmt, ap);
// NB. On Windows, vsnprintf returns -1 if the string didn't fit the
// buffer. On Linux & OSX, it returns the length it would have needed.
if (needed <= size && needed >= 0) {
// It fit fine the first time, we're done.
return std::string (&buf[0]);
} else {
// vsnprintf reported that it wanted to write more characters
// than we allotted. So do a malloc of the right size and try again.
// This doesn't happen very often if we chose our initial size
// well.
std::vector <char> buf;
size = needed;
buf.resize (size);
needed = vsnprintf (&buf[0], size, fmt, apcopy);
return std::string (&buf[0]);
}
}
Обновлено: когда я писал этот код, я понятия не имел, что для этого требуется соответствие C99 и что Windows (а также более старый glibc) имеет другое поведение vsnprintf, при котором он возвращает -1 в случае сбоя, а не окончательную меру того, сколько места необходим. Вот мой исправленный код, могли бы все его просмотреть, и если вы думаете, что это нормально, я снова отредактирую, чтобы сделать это единственной перечисленной стоимостью:
std::string
Strutil::vformat (const char *fmt, va_list ap)
{
// Allocate a buffer on the stack that's big enough for us almost
// all the time. Be prepared to allocate dynamically if it doesn't fit.
size_t size = 1024;
char stackbuf[1024];
std::vector<char> dynamicbuf;
char *buf = &stackbuf[0];
va_list ap_copy;
while (1) {
// Try to vsnprintf into our buffer.
va_copy(ap_copy, ap);
int needed = vsnprintf (buf, size, fmt, ap);
va_end(ap_copy);
// NB. C99 (which modern Linux and OS X follow) says vsnprintf
// failure returns the length it would have needed. But older
// glibc and current Windows return -1 for failure, i.e., not
// telling us how much was needed.
if (needed <= (int)size && needed >= 0) {
// It fit fine so we're done.
return std::string (buf, (size_t) needed);
}
// vsnprintf reported that it wanted to write more characters
// than we allotted. So try again using a dynamic buffer. This
// doesn't happen very often if we chose our initial size well.
size = (needed > 0) ? (needed+1) : (size*2);
dynamicbuf.resize (size);
buf = &dynamicbuf[0];
}
}
Хорошая работа над форматом / vs форматом. Возможно, stackoverflow нужен какой-то раздел для обмена фрагментами кода :-)
Кажется, что ваш код не будет работать, если результирующая строка больше 1024 байтов. Согласно MSDN: vsnprintf - Возвращаемое значение ... если количество символов для записи больше, чем count, эти функции возвращают -1, указывая, что вывод был усечен.
Зачем использовать переменный размер массива, если вы не собираетесь делать это в цикле? Нет необходимости копировать ap, что может быть дорогостоящим. На странице руководства GNU [v] s [n] printf есть пример того, как сделать это более переносимым: tin.org/bin/man.cgi?section=3&topic=snprintf
Кроме того, при создании строки используйте значение, возвращаемое vsnprintf. Это избавит вас от лишних вызовов strlen.
Андреас: Большое спасибо! Я не понимал, что vsnprintf имеет другое возвращаемое значение ошибки в Windows. Я отредактирую пример кода, чтобы отразить это.
Bklyn: спасибо за советы, снова связанные с vsnprintf, который не был C99. Не могли бы вы просмотреть мои правки и сказать, что вы думаете о новой версии?
Это было (почти) именно то, что мне было нужно. Спасибо. Я изменил его на std :: string strprintf (const char * fmt, ...), добавив va_list ap; и va_start (ap, fmt); до начала функции и va_end (ap) до конца. Я прервал цикл при успехе (вместо того, чтобы сразу вернуться), чтобы я мог вернуть строку после va_end (). Я также сделал все немного более консервативным, изменив размер stackbuf [] и аргумент resize () на размер + 2 (не уверен, что размер в порядке, размер + 1 определенно будет в порядке, размер + 2 удовлетворяет мою паранойю). Протестировал все это с помощью Visual C++ 2010, убедился, что выполнил цикл.
@Larry, +1 спасибо, нашел это и использовал его с несколькими модами, как указано в моем предыдущем комментарии (в котором используется ровно 600 символов, отсюда и этот комментарий :). Чтобы проверить это, я начал с крошечного автоматического буфера, и я могу подтвердить, что в случае сбоя Visual C++ 2010 требуется значение -1 и что цикл восстановления после сбоя работает нормально.
Я думаю, что тест на соответствие должен требовать, чтобы необходимый <размер не был меньше или равен, потому что размер включает NUL, а необходимый - нет. Если vsnprintf не работает по какой-либо другой причине, вам следует ограничить цикл удвоения размера некоторым верхним пределом. Кроме того, в этой ссылке MSDN говорится, что с VS 2015 и Windows 10 vsnprintf соответствует C99. msdn.microsoft.com/en-us/library/1kt27hek.aspx
В версии, которая поддерживает более старые версии, я счел необходимым обернуть vsnprintf
va_copy
, va_end
. Я обновил код соответствующим образом.
This code is part of an open source project that I'm writing
... эта библиотека сейчас общедоступна?
@kevinarpe Он доступен уже много лет. Но я воспроизвел соответствующую часть выше. Но в наши дни я настоятельно рекомендую использовать полностью типобезопасную версию C++, такую как tinyformat или другие подобные проекты.
Следующее может быть альтернативным решением:
void A::printto(ostream outputstream) {
char buffer[100];
string s = "stuff";
sprintf(buffer, "some %s", s);
outputstream << buffer << endl;
b.printto(outputstream);
}
(Аналогичный B::printto
) и определим
void A::print(FILE *f) {
printto(ofstream(f));
}
string A::to_str() {
ostringstream os;
printto(os);
return os.str();
}
Конечно, вам действительно следует использовать snprintf вместо sprintf, чтобы избежать переполнения буфера. Вы также можете выборочно изменить более рискованные sprintfs на формат <<, чтобы быть безопаснее и при этом изменять как можно меньше.
Я приму ваш ответ как комбинацию №1 и №2 :-) Есть ли у ofstream конструктор, который принимает дескрипторы файлов? У меня создалось впечатление, что они несовместимы ...
Вам следует попробовать заголовочный файл SafeFormat библиотеки Loki (http://loki-lib.sourceforge.net/index.php?n=Idioms.Printf). Он похож на библиотеку строковых форматов boost, но сохраняет синтаксис функций printf (...).
Надеюсь, это поможет!
Пожалуйста, не публикуйте просто какой-нибудь инструмент или библиотеку в качестве ответа. По крайней мере, продемонстрируйте как это решает проблему в самом ответе.
Библиотека {fmt} предоставляет функцию fmt::sprintf
, которая выполняет форматирование, совместимое с printf
(включая позиционные аргументы согласно Спецификация POSIX), и возвращает результат как std::string
:
std::string s = fmt::sprintf("The answer is %d.", 42);
Отказ от ответственности: Я автор этой библиотеки.
Хотя я уже ответил на этот вопрос, я также хотел бы указать на этот проект: github.com/c42f/tinyformat, который хорошо решает проблему и действительно отлично справляется с воспроизведением нотации форматирования printf. В наши дни я использую этот пакет напрямую, а не метод vsprintf, который я подробно описал несколько лет назад в своем другом ответе.