Как построить 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)

оба они являются примерами интерполяции строк.

Как я могу сделать это на С++?

Важные предостережения:

  1. Я знаю, что могу использовать std::cout, если я хочу вывести такое сообщение на стандартный вывод (не интерполяция строк, а вывод строки, которую я хочу):
cout << "error! value was " << actualValue << " but I expected "
<< expectedValue;

Я не хочу печатать строку в стандартный вывод. Я хочу передать std::string в качестве аргумента функции (например, конструктору объекта исключения).

  1. Я использую С++ 11, но переносимость потенциально проблематична, поэтому знание того, какие методы работают, а какие нет, в каких версиях С++, было бы плюсом.

Изменить

  1. Для моего непосредственного использования я не беспокоюсь о производительности (я делаю исключение, чтобы кричать вслух!). Однако в целом было бы очень полезно знать относительную производительность различных методов.

  2. Почему бы просто не использовать сам printf (в конце концов, C++ — надмножество C...)? В этом ответе обсуждаются некоторые причины, почему нет. Насколько я понимаю, безопасность типов является серьезной причиной: если вы поместите %d, переменная, которую вы туда поместите, должна действительно быть конвертируемой в целое число, поскольку именно так функция определяет, какой это тип. Было бы намного безопаснее иметь метод, который использует знания времени компиляции о фактическом типе вставляемых переменных.


person stochastic    schedule 21.06.2016    source источник
comment
C++ до сих пор не имеет стандартного способа сделать это? Я удивлен, ему удалось получить потоки, но не современный printf   -  person pm100    schedule 22.06.2016
comment
Как насчет того, чтобы просто использовать printf или fprintf(std::cout, ... в c++?   -  person Fantastic Mr Fox    schedule 22.06.2016
comment
Страуструпп собрал хороший пример использования функций шаблона с переменным числом аргументов в типобезопасном printf. Я действительно думаю, что это хорошая реализация.   -  person SergeyA    schedule 22.06.2016
comment
@ Бен: это отличный вопрос. Я добавил примечание по этой теме.   -  person stochastic    schedule 22.06.2016
comment
Кстати, это называется интерполяцией строк.   -  person user703016    schedule 22.06.2016
comment
@KretabChabawenizc спасибо, знать, что этот термин полезен. В том же ключе, что и комментарий pm100, я отмечаю, что страница википедии по интерполяции строк (ссылка) дает примеры для многих языков, но не C++   -  person stochastic    schedule 22.06.2016
comment
...%d это int, а не double.   -  person T.C.    schedule 15.11.2016


Ответы (5)


В C++20 вы сможете использовать std::format.

Это будет поддерживать форматирование в стиле Python:

string s = std::format("{1} to {0}", "a", "b");

Уже доступна реализация: https://github.com/fmtlib/fmt.

person Stephan Dollberg    schedule 20.07.2019
comment
См. также std::format() на сайте cppreference.com. Обратите внимание, что указывать индексы в заполнителях формата необязательно: string s1 = std::format("{} to {}", "a", "b"); - person Remy Lebeau; 08.10.2020

Способ 1. Использование потока строк

Похоже, std::stringstream дает быстрое решение:

std::stringstream ss;
ss << "error! value was " << actualValue << " but I expected " <<  expectedValue << endl;

//example usage
throw MyException(ss.str())

Положительно

  • нет внешних зависимостей
  • Я считаю, что это работает как в С++ 03, так и в С++ 11.

Исключение

  • как сообщается, довольно медленно
  • немного более запутанный: вы должны создать поток, записать в него, а затем получить из него строку.

Метод 2. Формат Boost

Также возможна библиотека Boost Format. Используя это, вы должны сделать:

throw MyException(boost::format("error! value was %1% but I expected %2%") % actualValue % expectedValue);

Положительно

  • довольно чистый по сравнению с методом stringstream: одна компактная конструкция

Исключение

  • как сообщается, довольно медленный: использует метод потока внутри
  • это внешняя зависимость

Изменить:

Способ 3: переменные параметры шаблона

Кажется, что типобезопасная версия printf может быть создана с использованием вариативных параметров шаблона (технический термин для шаблона, который принимает неопределенное количество параметров шаблона). Я видел ряд возможностей в этом ключе:

  • Этот вопрос дает компактный пример и обсуждает проблемы с производительностью в этом примере.
  • Этот ответ на этот вопрос, реализация которого также довольно компактна, но, как сообщается, все еще страдает от проблем с производительностью.
  • Библиотека fmt, обсуждаемая в этот ответ, как сообщается, довольно быстрый и кажется таким же чистым, как и сам printf, но является внешней зависимостью

Положительно

  • использование чистое: просто вызовите функцию, подобную printf
  • Сообщается, что библиотека fmt довольно быстра.
  • Остальные варианты кажутся довольно компактными (внешняя зависимость не требуется)

Исключение

  • библиотека fmt, хотя и быстрая, является внешней зависимостью
  • другие варианты, по-видимому, имеют некоторые проблемы с производительностью
person stochastic    schedule 21.06.2016
comment
стоит отметить, что первое решение будет очень сложно локализовать позже. Осмысленное сообщение разбивается на бессмысленные части, а для некоторых языков может потребоваться другой порядок параметров для формирования звукового сообщения. - person ; 22.06.2016
comment
Это хорошее замечание, хотя можно возразить, что второй путь на самом деле искать не легче. Глядя на такое сообщение об ошибке, я бы все равно искал то, что выглядит как постоянные части (т. Е. Я бы искал ошибку! значение было или но я ожидал). В идеале, само сообщение об ошибке должно иметь какой-то уникальный ведущий элемент, который можно было бы найти (например, ошибка № 5:), но это вопрос того, как структурированы ошибки... - person stochastic; 22.06.2016
comment
@deniss Я не понимаю, что вы имеете в виду, говоря, что для некоторых языков может потребоваться другой порядок параметров: разве мы не имеем дело только с C ++? - person stochastic; 22.06.2016
comment
@stochastic Некоторые языки как в реальных языках, а не в языках программирования. - person Fund Monica's Lawsuit; 22.06.2016
comment
@stochastic, неопределенная ссылка на грех дает лучшие результаты поиска, чем просто неопределенная ссылка, так что это зависит. Под языками я подразумеваю человеческие языки. Для некоторых языков может потребоваться другой порядок параметров, чтобы они не звучали как Йода. Некоторым может потребоваться дополнительная пунктуация после последнего параметра. - person ; 22.06.2016
comment
Да, лучше локализовать всю строку целиком. Альтернативой использованию boost::format() может быть просто std::string::replace() для заполнения заполнителей. , например: std::string s = "error! value was %1% but I expected %2%"; std::string::size_type idx = s.find("%1%"); s.replace(idx, 3, std::to_string(acutalValue)); idx = s.find("%2%"); s.replace(idx, 3,std::to_string(expectedValue)); throw MyException(s); Не так элегантно, как boost::format() или даже std::stringstream, но все же можно использовать в простых случаях. - person Remy Lebeau; 22.06.2016

В С++ 11 вы можете использовать std::to_string:

"error! value was " + std::to_string(actualValue) + " but I expected " + std::to_string(expectedValue)

Это некрасиво, но просто, и вы можете использовать макрос, чтобы немного уменьшить его. Производительность не велика, так как вы не reserve() места заранее. шаблоны Variadic, вероятно, были бы быстрее и выглядели бы лучше.

Такое построение строк (вместо интерполяции) также плохо для локализации, но вы, вероятно, воспользуетесь библиотекой, если вам это нужно.

person Peter Tseng    schedule 15.11.2016

Используйте все, что вам нравится:

1) стд::строковый поток

#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"));
person Vyacheslav Napadovsky    schedule 15.11.2016

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ:
Последующий код основан на статье, которую я прочитал 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's 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's 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's 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;
}
//<-----------------------------------------------------------------------------
person MABVT    schedule 21.02.2019