"интерполяция строк" в С++: построить std::string со встроенными значениями (например, для сообщений об ошибках)?
Я хочу создать строку со встроенной информацией. Один из способов (не единственный способ) достижения того, что я хочу, называется строковая интерполяция или подстановка переменных, где заполнители в строке заменяются фактическими значениями.
В C я бы сделал что-то вроде этого:
printf("error! value was %d but I expected %d",actualValue,expectedValue)
тогда как если бы я программировал в python, я бы сделал что-то вроде этого:
"error! value was {0} but I expected {1}".format(actualValue,expectedValue)
оба являются примерами строковой интерполяции.
Как это сделать в С++?
Важные предостережения:
- Я знаю, что я могу использовать
std::cout
, если я хочу напечатать такое сообщение на стандартный вывод (а не на строку интерполяции, но распечатывает нужный тип строки):
cout << "error! value was " << actualValue << " but I expected "
<< expectedValue;
Я не хочу печатать строку в stdout. Я хочу передать std::string
в качестве аргумента функции (например, конструктор объекта исключения).
- Я использую С++ 11, но переносимость потенциально является проблемой, поэтому знание того, какие методы работают и не работают, в каких версиях С++ будет плюсом.
Edit
-
Для моего непосредственного использования меня не интересует производительность (я поднимаю исключение для крика вслух!). Однако знание относительной производительности различных методов было бы очень полезно в целом.
-
Почему бы просто не использовать printf (С++ - это супермножество C в конце концов...)? В этом ответе обсуждаются некоторые причины, почему нет. Насколько я понимаю, тип безопасности - это большая причина: если вы положили% d, переменная, которую вы ввели, лучше всего была бы конвертируемой в целое число, так как функция определяет, какой тип она есть. Было бы гораздо безопаснее иметь метод, который использует знание времени компиляции фактического типа переменных, которые нужно вставить.
Ответы
Ответ 1
Способ 1: использование потока строк
Похоже, std::stringstream
дает быстрое решение:
std::stringstream ss;
ss << "error! value was " << actualValue << " but I expected " << expectedValue << endl;
//example usage
throw MyException(ss.str())
положительный
- нет внешних зависимостей
- Я считаю, что это работает как в C++ 03, так и в C++ 11.
отрицательный
- по сообщениям довольно медленно
- немного более грязно: вы должны создать поток, записать в него, а затем извлечь из него строку.
Способ 2: повышение формата
Библиотека Boost Format также возможна. Используя это, вы бы сделали:
throw MyException(boost::format("error! value was %1% but I expected %2%") % actualValue % expectedValue);
положительный
- довольно чистый по сравнению с методом stringstream: одна компактная конструкция
отрицательный
- как сообщается, довольно медленный: использует потоковый метод внутри
- это внешняя зависимость
Редактировать:
Способ 3: параметры шаблона
Похоже, что типобезопасная версия printf может быть создана с помощью переменных параметров шаблона (технический термин для шаблона, который принимает неопределенное количество параметров шаблона). Я видел ряд возможностей в этом ключе:
- Этот вопрос дает компактный пример и обсуждает проблемы производительности с этим примером.
- Этот ответ на этот вопрос, реализация которого также довольно компактна, но, по сообщениям, все еще страдает от проблем с производительностью.
- Библиотека fmt, обсуждаемая в этом ответе, по сообщениям, довольно быстрая и, кажется, такая же чистая, как и сам printf, но является внешней зависимостью
положительный
- использование чисто: просто вызовите функцию, похожую на printf
- Библиотека FMT по сообщениям довольно быстро
- Другие параметры кажутся довольно компактными (не требуется никаких внешних зависимостей)
отрицательный
- библиотека fmt, хотя и быстрая, является внешней зависимостью
- другие варианты, по-видимому, имеют некоторые проблемы с производительностью
Ответ 2
В С++ 11 вы можете использовать std::to_string
:
"error! value was " + std::to_string(actualValue) + " but I expected " + std::to_string(expectedValue)
Это не очень, но это просто, и вы можете использовать макрос, чтобы немного уменьшить его. Производительность невелика, так как вы заранее не занимаете пространство reserve()
. Шаблоны Variadic, вероятно, будут быстрее и выглядят лучше.
Этот тип строковой конструкции (вместо интерполяции) также плохо для локализации, но вы, вероятно, использовали бы библиотеку, если бы вам это нужно.
Ответ 3
Используйте все, что вам нравится:
1) std:: stringstream
#include <sstream>
std::stringstream ss;
ss << "Hello world!" << std::endl;
throw std::runtime_error(ss.str());
2) libfmt: https://github.com/fmtlib/fmt
#include <stdexcept>
throw std::runtime_error(
fmt::format("Error has been detected with code {} while {}",
0x42, "copying"));
Ответ 4
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Следующий код основан на статье, которую я прочитал 2 года назад. Я найду источник и выложу его как можно скорее.
Это то, что я использую в своем проекте C++ 17. Должен работать с любым компилятором C++, поддерживающим шаблоны переменных.
Использование:
std::string const word = "Beautiful";
std::string const message = CString::format("%0 is a %1 word with %2 characters.\n%0 %2 %0 %1 %2", word, "beautiful", word.size());
// Prints:
// Beautiful is a beautiful word with 9 characters.
// Beautiful 9 Beautiful beautiful 9.
Реализация класса:
/**
* The CString class provides helpers to convert 8 and 16-bit
* strings to each other or format a string with a variadic number
* of arguments.
*/
class CString
{
public:
/**
* Format a string based on 'aFormat' with a variadic number of arbitrarily typed arguments.
*
* @param aFormat
* @param aArguments
* @return
*/
template <typename... TArgs>
static std::string format(
std::string const&aFormat,
TArgs &&...aArguments);
/**
* Accept an arbitrarily typed argument and convert it to it proper
* string representation.
*
* @tparam TArg
* @tparam TEnable
* @param aArg
* @return
*/
template <
typename TArg,
typename TEnable = void
>
static std::string toString(TArg const &aArg);
/**
* Accept a float argument and convert it to it proper string representation.
*
* @tparam TArg
* @param arg
* @return
*/
template <
typename TArg,
typename std::enable_if<std::is_floating_point<TArg>::value, TArg>::type
>
static std::string toString(const float& arg);
/**
* Convert a string into an arbitrarily typed representation.
*
* @param aString
* @return
*/
template <
typename TData,
typename TEnable = void
>
static TData const fromString(std::string const &aString);
template <
typename TData,
typename std::enable_if
<
std::is_integral<TData>::value || std::is_floating_point<TData>::value,
TData
>::type
>
static TData fromString(std::string const &aString);
private:
/**
* Format a list of arguments. In this case zero arguments as the abort-condition
* of the recursive expansion of the parameter pack.
*
* @param aArguments
*/
template <std::size_t NArgs>
static void formatArguments(std::array<std::string, NArgs> const &aArguments);
/**
* Format a list of arguments of arbitrary type and expand recursively.
*
* @param outFormatted
* @param inArg
* @param inArgs
*/
template <
std::size_t NArgs,
typename TArg,
typename... TArgs
>
static void formatArguments(
std::array<std::string, NArgs> &aOutFormatted,
TArg &&aInArg,
TArgs &&...aInArgs);
};
//<-----------------------------------------------------------------------------
//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <typename... TArgs>
std::string CString::format(
const std::string &aFormat,
TArgs &&...aArgs)
{
std::array<std::string, sizeof...(aArgs)> formattedArguments{};
formatArguments(formattedArguments, std::forward<TArgs>(aArgs)...);
if constexpr (sizeof...(aArgs) == 0)
{
return aFormat;
}
else {
uint32_t number = 0;
bool readNumber = false;
std::ostringstream stream;
for(std::size_t k = 0; k < aFormat.size(); ++k)
{
switch(aFormat[k])
{
case '%':
readNumber = true;
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
// Desired behaviour to enable reading numbers in text w/o preceding %
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
if(readNumber)
{
number *= 10;
number += static_cast<uint32_t>(aFormat[k] - '0');
break;
}
default:
if(readNumber)
{
stream << formattedArguments[std::size_t(number)];
readNumber = false;
number = 0;
}
stream << aFormat[k];
break;
#pragma GCC diagnostic warning "-Wimplicit-fallthrough"
}
}
if(readNumber)
{
stream << formattedArguments[std::size_t(number)];
readNumber = false;
number = 0;
}
return stream.str();
}
}
//<-----------------------------------------------------------------------------
//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <typename TArg, typename enable>
std::string CString::toString(TArg const &aArg)
{
std::ostringstream stream;
stream << aArg;
return stream.str();
}
//<-----------------------------------------------------------------------------
//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <
typename TArg,
typename std::enable_if<std::is_floating_point<TArg>::value, TArg>::type
>
std::string CString::toString(const float& arg) {
std::ostringstream stream;
stream << std::setprecision(12) << arg;
return stream.str();
}
//<-----------------------------------------------------------------------------
//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <std::size_t argCount>
void CString::formatArguments(std::array<std::string, argCount> const&aArgs)
{
// Unused: aArgs
}
//<-----------------------------------------------------------------------------
//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <std::size_t argCount, typename TArg, typename... TArgs>
void CString::formatArguments(
std::array<std::string, argCount> &outFormatted,
TArg &&inArg,
TArgs &&...inArgs)
{
// Executed for each, recursively until there no param left.
uint32_t const index = (argCount - 1 - sizeof...(TArgs));
outFormatted[index] = toString(inArg);
formatArguments(outFormatted, std::forward<TArgs>(inArgs)...);
}
//<-----------------------------------------------------------------------------
//<-----------------------------------------------------------------------------
//<
//<-----------------------------------------------------------------------------
template <
typename TData,
typename std::enable_if
<
std::is_integral<TData>::value || std::is_floating_point<TData>::value,
TData
>::type
>
TData CString::fromString(std::string const &aString)
{
TData const result{};
std::stringstream ss(aString);
ss >> result;
return result;
}
//<-----------------------------------------------------------------------------
}
Ответ 5
В С++ 20 вы сможете использовать std::format
.
См. Http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r9.html для принятой статьи.
Это будет поддерживать форматирование в стиле Python:
string s1 = std::format("{1} to {0}", "a", "b");
Уже есть доступная реализация: https://github.com/fmtlib/fmt