为什么ostream_iterator需要显式声明要输出的对象类型?

发布于 2024-10-05 23:28:13 字数 1009 浏览 4 评论 0 原文

在当前的 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; 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);

我更通用、更优雅,对吗?

In current C++, the class ostream_iterator was designed like the following:

// excerpted from the standard C++

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

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

To me, this design is suboptimal. Because the user must specify the type T when declaring an ostream_iterator like this: ostream_iterator<int> oi(cout); In fact, cout can take any type of object as its argument, rather than only one type. This is an obvious restriction.

// 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&);
    ...
};

Now, we can use it as follows:

ostream_iterator oi(cout);

I think it is more generic and more elegant than

ostream_iterator<int> oi(cout);

Am I right?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

乙白 2024-10-12 23:28:13

简单的答案是,迭代器具有关联的类型,而 ostream_iterator 在概念上违反了迭代器的概念,因为即使不需要,也需要一个值类型。 (这基本上是@pts的答案)

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

它也是向后兼容的,因为 void 本来就不是一个有用的实例化。此外,void 参数也是默认参数。例如 template 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):
模板 struct ostream_iterator{...},我们可以更进一步,完全省略参数:

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

问题的根源和可能的出路

最后,在我看来,问题也可能是从概念上讲,在 STL 中,人们期望迭代器具有关联的明确的 value_type,即使它不像这里那样是必要的。从某种意义上说,ostream_iterator 违反了迭代器的一些概念。

因此,这种用法有两件事在概念上是错误的:1)当一个人复制时,他期望知道源的类型(容器 value_type)和目标类型 2)一个人没有复制首位!。在我看来,这种典型用法存在双重设计错误。应该有一个 std::send 可以直接与模板移位 << 运算符一起使用,而不是将 = 重定向到 << 就像 ostream_iterator 一样。

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 magic:

    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的类型都不存在困境。

The simple answer is that iterator have associated types and ostream_iterator conceptually violates the concept of an iterator by requiring a value_type even when it is not necessary. (This is basically @pts's answer)

What you are proposing is related to the idea behind the new "transparent operators", such as the new std::plus<void>. Which consist in having a special instantiation whose member function has a delayed type deduction.

It is also backward compatible because void is not a useful instantiation to begin with. Moreover the void parameter is also the default. For example template<T = void> struct std::plus{...} is the new declaration.


A possible implementation of a transparent ostream_iterator

Going back of std::ostream_iterator, an important test is whether we want to make it work with std::copy as std::ostream_iterator is usually used:

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

The technology for a transparent std::ostream_iterator is not there yet, because this fails:

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

To make this work, one can explicitly define the void instance. (This completes @CashCow 's answer)

#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;}
    };

}

Now this works:

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

Moreover, if we convince the standard committee to have a default void parameter (as they did with with std::plus):
template<class T = void, ...> struct ostream_iterator{...}, we could go a step further and omit the parameter altogether:

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

The root of the problem and a possible way out

Finally, in my opinion the problem might also be conceptual, in STL one expects an iterator to have a definite value_type associated even if it is not necessary like here. In some sense ostream_iterator violates some concepts of what is an iterator.

So there are two things that are conceptually wrong in this usage: 1) when one copies one expects to know the type of the source (container value_type) and target types 2) one is not copying anything in the first place!. In my opinion there is a double design mistake in this typical usage. There should be a std::send that works with a template shift << operators directly, instead of making = redirect to << as ostream_iterator does.

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

(The last argument should fulfill some kind of Sink concept).


** Using std::accumulate instead and a possible implementation of
std::send **

From a conceptual point of view, sending objects to a stream is more of an "accumulate" operation than a copy operator, so in principle std::accumulate should be a more suitable candidate, besides we don't need "target" iterators for it.
The problem is that std::accumulate wants to make copies of every object that is being accumulated, so this doesn't work:

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

To make it work we need to do some reference_wrapper magic:

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

Finally, the code can be simplified by having the equivalent of std::plus for the shift operator, in modern C++ this should look like this 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_;
        }
    };

}

Which can be used as:

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

Finally we can define:

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});
    }
}

Which can be used as

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

Finally, there is no dilemma about the type of any output_iterator here.

绝不服输 2024-10-12 23:28:13

看来你可能是对的。

让我们看看是否可以构造一个不需要模板参数的 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= 不应重载运算符=(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来提取元素类型,而上面的类不包含元素类型。

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

It appears you could be right.

Let's see if we can construct an ostream_iterator that does not need a template argument.

The iterator works by copying values into it, so *iter = x; ++iter;
The iterator cheats by making operator* return itself and ++iter also returning itself without changing any state. The "magic" is in the operator= which performs the output.

The "cout" must be a class member of type ostream*. It needs to be a pointer as iterators must be assignable, thus we assign the member (call it os) to the address of the stream passed in.

So we would overload operator= this way:

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

Note that the templatised operator= should not oveload operator=(our_ostream_iterator const&) which is more specialised than the template.

You would still want a template on the element type so we will call that our_basic_ostream_iterator

ostream_iterator would still remain a template class on its element type. Thus:

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
};

and then of course

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

The drawback of all of this though is that the above does not conform to all the properties of iterators such that it could be passed to any algorithm / class that requires a forward iterator. Why? Because such an algorithm should be able to invoke iterator_traits to extract the element type and the class above does not contain an element type.

It would lead to compile-time errors in the algorithm that is using your iterator and would potentially be hard to track down the reason why.

江城子 2024-10-12 23:28:13

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

为一组模板参数实例化 operator < 存在危险,这与实例化 operator *operator++ 所使用的参数不同

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

I think the reason is that it has other members also. Obviously the entire set of member functions need to be consistent in their behavior for a given set of T and other template arguments.

There's danger in operator < being instantiated for a set of template arguments which is different from what is used to instantiate operator * or operator++

Hence, the individual methods are not template themselves and rather the entire class is a template so ensure uniform T and other template arguments.

绝影如岚 2024-10-12 23:28:13

是的你是对的。按照你的建议,它会更灵活。然而,它的设计方式更适合 STL 使用迭代器的方式:数据类型 (T) 的一种迭代器类型。

Yes, you are right. It would be more flexible as you suggest. However, the way it's designed fits more closely to how STL uses iterators: one iterator type for data type (T).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文