【问题标题】:Why does ostream_iterator need to explicitly declare the type of objects to output?为什么 ostream_iterator 需要显式声明要输出的对象类型?
【发布时间】:2011-05-19 00:02:15
【问题描述】:

在当前的 C++ 中,类 ostream_iterator 的设计如下:

// excerpted from the standard C++

template<class T, ...>
class ostream_iterator
{
public:
    ostream_iterator(ostream_type&);
    ...

    ostream_iterator<T,...>& operator =(const T&);
    ...
};

对我来说,这种设计并不理想。因为用户在像这样声明 ostream_iterator 时必须指定类型 T:ostream_iterator&lt;int&gt; oi(cout); 实际上,cout 可以将任何类型的对象作为其参数,而不仅仅是一种类型。这是一个明显的限制。

// Below is my own version

// doesn't need any template parameter here
class ostream_iterator
{
public:
    ostream_iterator(ostream_type&);
    ...

    // define a template member function which can take any type of argument and output it
    template<class T> 
    ostream_iterator<T,...>& operator =(const T&);
    ...
};

现在,我们可以如下使用它:

ostream_iterator oi(cout);

我觉得比

更通用更优雅
ostream_iterator<int> oi(cout);

我说的对吗?

【问题讨论】:

  • @Charles Bailey:有很多技巧可以告诉提取器下一个对象的类型是什么。例如,您可以使用整数作为类型标识符来指示下一个对象的类型。
  • 我们不需要关心提取器如何从流中读取对象。就像你写 cout
  • @Charles Bailey:我们讨论的是 ostream_iterator 的设计,而不是如何使用它。我认为后者比当前的设计更通用、更优雅。想象一下,我们有一个算法可以将几种类型的对象输出到 ostream,这样讨论就有意义了。
  • @Nim:使用后一个版本不会破坏现有的标准容器和算法,它可以在当前版本工作的任何情况下工作。但后者对我来说更漂亮。
  • @MSalters:value_typevoid(它是一个输出迭代器,24.3.1/1)

标签: c++ stream


【解决方案1】:

简单的答案是iterator 具有关联类型,而ostream_iterator 在概念上违反了迭代器的概念,即使在没有必要的情况下也需要value_type。 (这基本上是@pts 的回答)

您提出的内容与新的“透明运算符”背后的想法有关,例如新的std::plus&lt;void&gt;。其中包括具有一个特殊的实例化,其成员函数具有延迟类型推导。

它也是向后兼容的,因为void 一开始就不是一个有用的实例化。此外,void 参数也是默认值。例如template&lt;T = void&gt; struct std::plus{...} 是新的声明。


透明ostream_iterator的可能实现

回到std::ostream_iterator,一个重要的测试是我们是否想让它与std::copy一起工作,因为通常使用std::ostream_iterator

std::vector<int> v = {...};
std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));

透明std::ostream_iterator 的技术尚不存在,因为它失败了:

std::copy(v.begin(), v.end(), std::ostream_iterator<void>(std::cout, " "));

要完成这项工作,可以显式定义void 实例。 (这完成了@CashCow 的回答)

#include<iterator>
namespace std{
    template<>
    struct ostream_iterator<void> : 
        std::iterator<std::output_iterator_tag, void, void, void, void>
    {
        ostream_iterator(std::ostream& os, std::string delim) : 
            os_(os), delim_(delim)
        {}
        std::ostream& os_;
        std::string delim_;
        template<class T> ostream_iterator& operator=(T const& t){
            os_ << t << delim_;
            return *this;
        }
        ostream_iterator& operator*(){return *this;}
        ostream_iterator& operator++(){return *this;}
        ostream_iterator& operator++(int){return *this;}
    };

}

现在可以了:

std::copy(v.begin(), v.end(), std::ostream_iterator<void>(std::cout, " "));

此外,如果我们说服标准委员会有一个默认的void 参数(就像他们对std::plus 所做的那样): template&lt;class T = void, ...&gt; struct ostream_iterator{...},我们可以更进一步,完全省略参数:

std::copy(v.begin(), v.end(), std::ostream_iterator<>(std::cout, " "));

问题的根源和可能的出路

最后,在我看来,这个问题也可能是概念性的,在 STL 中,人们期望迭代器有一个明确的 value_type 关联,即使这里没有必要也是如此。在某种意义上ostream_iterator 违反了迭代器的一些概念。

所以在这种用法中有两件事在概念上是错误的:1)当一个人希望知道源(容器value_type)和目标类型的类型时,一个复制者一开始就没有复制任何东西!.在我看来,这种典型用法存在双重设计错误。应该有一个std::send 可以直接与模板移位&lt;&lt; 运算符一起使用,而不是像ostream_iterator 那样使= 重定向到&lt;&lt;

std::send(v.begin(), v.end(), std::cout); // hypothetical syntax
std::send(v.begin(), v.end(), std::ostream_receiver(std::cout, " ")); // hypothetical syntax
std::send(v.begin(), v.end(), 'some ostream_filter'); // hypothetical syntax

(最后一个参数应该满足某种Sink 概念)。


** 改用std::accumulate 并可能实现 std::send**

从概念的角度来看,将对象发送到流中更多的是“累积”操作而不是复制操作符,因此原则上std::accumulate 应该是更合适的候选者,此外我们不需要“目标”它的迭代器。 问题是std::accumulate 想要复制每个正在积累的对象,所以这不起作用:

    std::accumulate(e.begin(), e.end(), std::cout, 
        [](auto& sink, auto const& e){return sink << e;}
    ); // error std::cout is not copiable

为了让它发挥作用,我们需要做一些reference_wrapper 魔术:

    std::accumulate(e.begin(), e.end(), std::ref(std::cout), 
        [](auto& sink, auto const& e){return std::ref(sink.get() << e);}
    );

最后,代码可以通过等效于 std::plus 的移位运算符来简化,在现代 C++ 中,这应该看起来像这样 IM:

namespace std{

    template<class Sink = void, class T = void>
    struct put_to{
        std::string delim_;
        using sink_type = Sink;
        using input_type = T;
        Sink& operator()(Sink& s, T const& t) const{
            return s << t << delim_;
        }
    };

    template<>
    struct put_to<void, void>{
        std::string delim_;
        template<class Sink, class T>
        Sink& operator()(Sink& s, T const& t){
            return s << t;
        }
        template<class Sink, class T>
        std::reference_wrapper<Sink> operator()(std::reference_wrapper<Sink> s, T const& t){
            return s.get() << t << delim_;
        }
    };

}

可以用作:

std::accumulate(e.begin(), e.end(), std::ref(std::cout), std::put_to<>{", "});

最后我们可以定义:

namespace std{
    template<class InputIterator, class Sink>
    Sink& send(InputIterator it1, InputIterator it2, Sink& s, std::string delim = ""){
        return std::accumulate(it1, it2, std::ref(s), std::put_to<>{delim});
    }
}

可以用作

std::send(e.begin(), e.end(), std::cout, ", ");

最后,这里没有任何output_iterator的类型问题。

【讨论】:

    【解决方案2】:

    看来你可能是对的。

    让我们看看我们是否可以构造一个不需要模板参数的 ostream_iterator。

    迭代器通过将值复制到其中来工作,所以*iter = x; ++iter; 迭代器通过让 operator* 返回自身和 ++iter 也返回自身而不改变任何状态来作弊。 “魔法”在执行输出的 operator= 中。

    “cout”必须是 ostream* 类型的类成员。它需要是一个指针,因为迭代器必须是可分配的,因此我们将成员(称为 os)分配给传入的流的地址。

    所以我们会这样重载 operator=:

    template< typename T >
    our_ostream_iterator& operator=( const T& t )
    {
       (*os) << t;
       if( delim )
          (*os) << delim;
       return *this;
    }
    

    请注意,模板化的 operator= 不应超载 operator=(our_ostream_iterator const&) ,它比模板更专业。

    您仍然需要元素类型的模板,因此我们将其称为 our_basic_ostream_iterator

    ostream_iterator 仍将保留其元素类型的模板类。因此:

    template< typename E, typename TR=char_traits<E> >
    class our_basic_ostream_iterator : public std::iterator< /*traits here*/ >
    {
    public:
       typedef E element_type;
       typedef TR traits_type;
       typedef basic_ostream< E, TR > stream_type;
    private:
       stream_type * os;
       const E* delim;
    public:
       our_basic_ostream_iterator( stream_type s, const E* d = nullptr ) :
          os( &s ), delim( d )
       {
       }
    
       our_basic_ostream_iterator& operator++() { return *this; }
       our_basic_ostream_iterator operator++(int) { return *this; }
       our_basic_ostream_iterator& operator*() { return *this; }
    
       template< typename T >
       our_basic_ostream_iterator& operator=( const T& t ); // as above
    };
    

    当然还有

    typedef our_basic_ostream_iterator<char> our_ostream_iterator;
    typedef our_basic_ostream_iterator<wchar_t> our_wostream_iterator;
    

    所有这些的缺点是上面不符合迭代器的所有属性,因此它可以传递给任何需要前向迭代器的算法/类。为什么?因为这样的算法应该能够调用 iterator_traits 来提取元素类型,并且上面的类不包含元素类型。

    这会导致使用您的迭代器的算法出现编译时错误,并且可能难以追查原因。

    【讨论】:

    • 不过,这仍然必须用作our_ostream_iterator&lt;something&gt;,这是提问者想要避免的。为了有机会在不命名其类型的情况下使用它,您还需要某种包装函数来推断模板参数(然后 C++0x autodecltype 意味着您永远不需要使用该类型,而在 C++03 中,您有时仍需要将其拼写出来)。
    • 我不确定 OP 是否需要从构造函数推断元素类型和宽输入/输出,但您可以使用函数来推断要制作和使用的类型,具体取决于您的流进来了。
    • 你通过值传递stream_type s并将其地址保存到os
    • 这个答案声称“构造一个不需要模板参数的 ostream_iterator。”,但显然在 our_basic_ostream_iterator 的定义中有 typename E。这是怎么回事?
    • OP 表示流式传输的类型。我的代码中的模板参数是元素类型的参数,即它是 char 还是 wchar_t,你可以 typedef 它,因为你现在可以了。字符类型虽然应该可以从您可以使用 auto make_ostream_iterator(...) 的流对象派生,
    【解决方案3】:

    我认为原因是它还有其他成员。显然,对于给定的一组 T 和其他模板参数,整组成员函数的行为需要保持一致。

    operator &lt; 被实例化的一组模板参数与用于实例化 operator *operator++ 的模板参数不同

    因此,各个方法本身不是模板,而是整个类都是模板,因此请确保统一的 T 和其他模板参数。

    【讨论】:

      【解决方案4】:

      是的,你是对的。正如你所建议的那样,它会更灵活。但是,它的设计方式更符合 STL 使用迭代器的方式:数据类型 (T) 的一种迭代器类型。

      【讨论】:

        猜你喜欢
        • 2012-08-17
        • 2011-04-16
        • 1970-01-01
        • 2012-04-05
        • 2019-11-20
        • 2016-04-05
        • 2010-11-10
        • 2018-01-22
        • 1970-01-01
        相关资源
        最近更新 更多