实现字符串视图格式化流输出

11
在实现C++1z的std::basic_string_view以便在旧编译器上使用时,我遇到了一个问题,即它的流输出运算符重载link1。基本上,它必须输出由string_view引用的内容,同时不依赖于任何空终止符(因为string_view不能保证是空终止的)。
通常,编写operator<<的重载非常容易,因为您可以依赖已经存在的重载,因此不需要使用entry对象如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的实现位于这个文件中, 但它大量使用了内部函数来进行格式化。我需要自己重新实现所有的格式化吗?


1
根据http://en.cppreference.com/w/cpp/io/basic_ostream/operator_ltlt2,char*的运算符<<可以使用Traits类型进行参数化,并且traits::length(s)个字符将被打印。这是否解决了您的问题? - Alexey Guseynov
@mariusm 但这只是延续了问题。我仍然需要执行格式化,这就是我在此询问如何在不访问内部未记录状态的情况下完成的。 - nshct
1
我并没有看到任何避免自己处理格式的真正方法。这可能听起来有些奇怪(对我来说确实有点),但有时候生活就是这样。幸运的是,字符串的格式化要求相当简单。 - Jerry Coffin
@mariusm 只有一个重载可以接受一个特征类型作为类型参数。它是在内部构造的。正如我所说的,没有办法提供我的运行时长度值。如果假设存在另一个格式化输出函数,可以采用特征对象实现这一点。但是现在还没有这个。 - nshct
1
您可以使用 ios_base::width() 获取当前宽度。您还需要查看 left vs. right,以及(如果记忆无误)fixed,以确定在哪里插入填充,并且是否要截断到字段宽度(使用 ios_base::fmtflags() 检索)。嗯...哦,如果您要进行填充,还需要读取当前的填充字符(使用 ios_base::fill())。 - Jerry Coffin
显示剩余7条评论
2个回答

8
据我所见,您需要自行处理这个问题。幸运的是,对于类似字符串的项所需进行的格式化相当简单 - 大多数情况下只需在字符串前后插入填充即可。要确定是否需要填充,您需要使用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对象。如所示,创建和使用它非常简单,但如果将其删除,它无疑会快一点。


1

在我看来,您只需要让插入运算符依赖于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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接