将字符传递给运算符重载的自定义流操纵器

Custom stream manipulator that passes a character to operator overload

提问人:joaocandre 提问时间:11/27/2021 更新时间:11/27/2021 访问量:210

问:

我正在玩弄 shift/io 流运算符重载,我想知道是否有办法将其他参数传递给函数,同时仍然为更简单的语法定义默认值?

考虑一个简单的例子:

#include <vector>
#include <iostream>

inline std::ostream& operator<<(std::ostream& ostream, const std::vector< int >& data) {
    ostream << data[0];
    for (int idx = 1; idx < data.size(); idx++) {
        ostream << "," << data[idx];   // pass ',' as argument?
    }
    return ostream;
}

我希望将分隔符字符传递给函数,例如,可能通过流修饰符:,

std::cout << std::vector<int>(3, 15) << std::endl;   // 15,15,15
std::cout << delimiter(;) << std::vector<int>(3,15) << std::endl;  // 15;15;15

我编写了一个简单的类来做到这一点,但生成的语法不是很干净(需要首先调用强制成员运算符重载):

#include <string>
#include <vector>
#include <iostream>
#include <sstream>

class formatted{
 public:
    explicit formatted(const std::string& sequence = ",") : delimiter(sequence) { }

    template < typename T >
    std::string operator<<(const T& src) const {
        std::stringstream out;
        if (src.size()) {
            out << src[0];
            for (int i = 1; i < src.size(); i++) {
                out << delimiter << src[i];
            }
        }
        return out.str();
    }

 protected:
    std::string delimiter;
};


template < typename T >
inline std::ostream& operator<<(std::ostream& ostream, const std::vector< T >& data) {
    ostream << (formatted() << data);
    return ostream;
}



int main(int argc, char const *argv[]) {
    std::vector< int > data(10, 5);

    std::cout << data << std::endl;  // 5,5,5...5,5
    std::cout << (formatted("/") << data) << std::endl;  // 5/5/5...5/5

    return 0;
}

有没有办法简化这一点,不需要帮助程序类,或者通过使用传统的流操纵器?

C++ 运算符重载 IOSTREAM 操纵器

评论


答:

2赞 Jan Schultke 11/27/2021 #1

对当前设计的细微改进

你正在做的事情是可能的,不能再简化了。如果你想坚持你目前的实现,我建议修复以下问题:

不必要地复制整个字符串

return std::move(out).str();

以防止在末尾复制整个字符串(从 C++20 开始)。stringstream

有符号/无符号比较

for (int i = 1; i < data.size(); ...)

导致在有符号和无符号之间进行比较,从而导致编译器警告处于足够的警告级别。 喜欢。std::size_t i

不受约束的流插入运算符

operator<<(const T&)

不受约束,可以接受任何类型,即使它仅适用于向量。 喜欢:

operator<<(const std::vector<T>&)

使用 代替std::stringstd::string_view

const std::string& sequence = ","

导致创建不必要的字符串,可能涉及堆分配,即使我们使用字符串文字(例如 . 通常我们更喜欢 ,但在这里,您将此序列存储在类中。这可能会产生问题,因为对字符串没有所有权,因此字符串的生存期可能会在使用视图之前过期。 此问题是设计所固有的,不容易解决。","std::string_viewstd::string_view

替代设计

调用流操纵器是不准确的,因为流操纵器是接受并返回(或派生类型)的函数。您的类型只是一些带有运算符重载的类型,但您也可以这样做:formattedbasic_iosformatted<<

std::cout << vector_to_string(data, "/");

如果未提供任何参数,则默认为 where。 问题是我们仍然没有使用流来输出字符,但我们首先必须创建一个字符串,将向量内容写入其中,然后将字符串写入流中。"/"","

相反,我们可以创建一个函数,该函数接受流作为其第一个参数并直接写入它。 其优点包括:

  • 删除,可能只是包括#include <sstream><iosfwd>
  • 性能改进
  • 可能没有堆分配
  • 没有不必要的字符串复制(在 C++20 之前)
  • 整体较短的代码

这种设计作为常规函数在标准库中也有先例。一个典型的例子是签名:std::getline

template < class CharT, ...>
std::basic_istream<CharT, ...>& getline( std::basic_istream<CharT, ...>& input,
                                         std::basic_string<CharT, ...>& str,
                                         CharT delim );

通过这种设计,我们可以创建更简洁的代码,这也解决了以下问题:

  • 无法使用std::string_view
  • 必须使用 A 来存储整个打印的矢量std::string
#include <string>
#include <vector>
#include <iostream>

template < typename T >
std::ostream& print(std::ostream& ostream,
                    const std::vector< T >& data,
                    std::string_view delimiter = ",")
{
    if (not data.empty()) {
        ostream << data.front();
        for (std::size_t i = 1; i < data.size(); i++) {
            ostream << delimiter << data[i];
        }
    }
    return ostream;
}


int main(int argc, char const *argv[])
{
    std::vector< int > data(10, 5);

    print(std::cout, data) << std::endl;
    print(std::cout, data, "/") << std::endl;
}

对任何范围的进一步泛化

我们可以推广此代码,使其适用于提供输入迭代器的任何范围,而不仅仅是 . 这意味着我们可以将它用于 、 、 等类型。std::vectorstd::arraystd::stringstd::vectorstd::list

template <typename Range>
std::ostream& print(std::ostream& ostream,
                    const Range& range,
                    std::string_view delimiter = ",")
{
    auto begin = range.begin();
    auto end = range.end();

    if (begin != end) {
        ostream << *begin;
        while (++begin != end) {
            ostream << delimiter << *begin;
        }
    }
    return ostream;
}

评论

0赞 joaocandre 11/28/2021
令人难以置信的信息量很大的答案,谢谢。请注意,分隔符不一定是字符串,一个对象就足以满足大多数用途,并且可以消除与成员一起使用类所附带的任何性能问题。charstd::string
1赞 arutar 11/27/2021 #2

一些操作输入/输出的功能

#include <iostream>
#include <vector>
#include <string>

std::string Delimiter=" ";

template <typename T>
inline std::ostream& operator<<(std::ostream& ostream, const std::vector<T>& v) {
    if (!v.empty()) {
        std::ostream_iterator<T> ost(ostream, Delimiter.data());

        if (v.size() > 1)
            std::copy(begin(v), end(v) - 1, ost);
        ostream << v.back();
    }

    return ostream;
}


int main() {
    std::cout << std::vector<int>(3, 15) << std::endl;
    Delimiter = "!";
    std::cout << std::vector<int>(5, 15) << std::endl;
    return 0;
}

当然,分隔符变量不是很漂亮,但是...... 输出

15 15 15
15!15!15!15!15