Реализация вывода потока в формате string_view

При реализации C++1z std::basic_string_view чтобы использовать его на старых компиляторах, я столкнулся с проблемой перегрузки для оператора потокового вывода. По сути, он должен выводить содержимое, на которое ссылается string_view не полагаясь на присутствие нулевого терминатора (как string_view не гарантируется прекращение

Обычно написание перегрузок для operator<< это довольно просто, так как вы можете полагаться на уже существующие перегрузки, и, следовательно, нет необходимости использовать сторожевые объекты, как упоминалось в этом вопросе о SO.

Но в этом случае нет предопределенной перегрузки для operator<< взятие указателя символа и длины ( очевидно). Из-за этого я создаю временный std::string экземпляр в моей текущей реализации:

template< typename TChar, typename TTraits >
auto operator<<(::std::basic_ostream<TChar, TTraits>& p_os, basic_string_view<TChar, TTraits> p_v)
    -> ::std::basic_ostream<TChar, TTraits>&
{
    p_os << p_v.to_string(); // to_string() returns a ::std::string.
    return p_os;
}

Это работает, но мне очень не нравится тот факт, что я должен создать временный std::string Например, это влечет за собой избыточное копирование данных и потенциальное использование динамической памяти. Это, по моему мнению, по крайней мере, отрицательно сказывается на цели использования облегченного ссылочного типа.

Итак, мой вопрос:

Каков наилучший способ реализовать правильный форматированный вывод для моего string_view без издержек?


Во время исследования я обнаружил, что LLVM делает это так: (находится здесь)

// [string.view.io]
template<class _CharT, class _Traits>
basic_ostream<_CharT, _Traits>&
operator<<(basic_ostream<_CharT, _Traits>& __os, basic_string_view<_CharT, _Traits> __sv)
{
    return _VSTD::__put_character_sequence(__os, __sv.data(), __sv.size());
}

Реализация __put_character_sequence находится в этом файле, но он интенсивно использует внутренние функции для форматирования. Нужно ли переопределять все форматирование самостоятельно?

2 ответа

Решение

Насколько я понимаю, вам придется справиться с этим самостоятельно.

К счастью, форматирование, которое вам нужно сделать для строкового элемента, довольно минимально - в большинстве случаев вставка отступа до или после строки, если это необходимо.

  • Чтобы выяснить, требуется ли заполнение, вам нужно получить текущее поле потока, используя ios_base::width(),
  • Чтобы выяснить, вставлять ли это до или после записи строки, вам нужно получить левый / правый флаги с ios_base::fmtflags(),
  • Чтобы выяснить, что вставить в качестве отступа, вы можете позвонить ios_base::fill(),
  • Наконец, я считаю, что вам нужно проверить fixed флаг - если память служит, с ней установлено, вам нужно обрезать строку, если она длиннее текущей ширины поля.

Так (с ультра-упрощенной реализацией string_view) код может выглядеть примерно так:

#include <iostream>
#include <iomanip>
#include <ios>
#include <sstream>

class string_view { 
    char const *data;
    size_t len;
public:
    string_view(char const *data, size_t len) : data(data), len(len) {}

    friend std::ostream &operator<<(std::ostream &os, string_view const &sv) { 
        std::ostream::sentry s{ os };
        if (s) {
            auto fill = os.fill();
            auto width = os.width();
            bool left = os.flags() & std::ios::left;
            bool right = os.flags() & std::ios::right;
            bool fixed = os.flags() & std::ios::fixed;

            auto pad = [&](size_t width) { while (width--) os.put(fill); };

            if (sv.len < width) {
                auto padding_len = width - sv.len;
                if (right) pad(padding_len);
                os.write(sv.data, sv.len);
                if (left) pad(padding_len);
            }
            else {
                os.write(sv.data, fixed ? width : sv.len);
            }
        }
        os.width(0);
        return os;
    }
};

#ifdef TEST   
void check(std::stringstream &a, std::stringstream &b) {
    static int i;

    ++i;
    if (a.str() != b.str()) {
        std::cout << "Difference in test:" << i << "\n";
        std::cout << "\"" << a.str() << "\"\n";
        std::cout << "\"" << b.str() << "\"\n";
    }
    a.seekp(0);
    b.seekp(0);
}

int main() { 
    char string[] = "Now is the time for every good man to come to the aid of Jerry.";

    std::stringstream test1;
    std::stringstream test2;

    test1 << string_view(string, 3);
    test2 << std::string(string, 3);
    check(test1, test2);

    test1 << string_view(string + 4, 2);
    test2 << string_view(string + 4, 2);
    check(test1, test2);

    test1 << std::setw(10) << std::left << string_view(string, 6);
    test2 << std::setw(10) << std::left << std::string(string, 6);
    check(test1, test2);

    test1 << std::setw(10) << std::right << string_view(string, 6);
    test2 << std::setw(10) << std::right << std::string(string, 6);
    check(test1, test2);

    test1 << std::setw(10) << std::right << string_view(string, sizeof(string));
    test2 << std::setw(10) << std::right << std::string(string, sizeof(string));
    check(test1, test2);

    test1 << std::setw(10) << std::right << std::fixed << string_view(string, sizeof(string));
    test2 << std::setw(10) << std::right << std::fixed << std::string(string, sizeof(string));
    check(test1, test2);
}
#endif

Ох - еще одна деталь. Так как мы пишем только в поток, а не напрямую в нижележащий буфер, я думаю, что на самом деле нам не нужно создавать sentry объект в этом случае. Как показано, его создание и использование довольно тривиально, но с удалением это, несомненно, будет хоть немного быстрее.

Мне кажется, что вы должны просто иметь возможность полагаться на свой оператор вставки на std::ostream::write. Что-то вроде:

      template<class _CharT, class _Traits>
basic_ostream<_CharT, _Traits>&
operator<<(basic_ostream<_CharT, _Traits>& __os, basic_string_view<_CharT, _Traits> __sv)
{
    __os.write(__sv.data(), __sv.size());
    return __os;
}
Другие вопросы по тегам