Why is ranges::ostream_iterator default-constructible?
Asked Answered
C

2

12

This question follows a discussion in the comments here.

In Eric Niebler's ranges-v3 library (which is sort-of becoming part of the standard for C++20), ranges::ostream_iterator is default-constructible - without an ostream.

How come?

I thought that "dummy" construction with effective construction later is an anti-pattern in C++, a wart we are gradually getting rid of. std::ostream iterator can only be constructed with a stream (for now - before C++20). And it's not as though we can do anything with the default-constructed range::ostream_iterator... So, what's the deal?

Charlacharlady answered 10/5, 2019 at 21:54 Comment(7)
There is nothing in this question about C++20. The Range v3 library is not the same thing as C++20.Nealson
@NicolBolas Except Ranges is in C++20, which brings with it a default-constructible std::ostream_iterator.Interpretive
@Barry: C++20 has ranges in it, but it doesn't have Ranges-v3-the-library in it. It has a very similar library in it, but it's not the same thing.Nealson
@Nicol I don't understand the distinction you're making. C++20 will have a default-constructible std::ostream_iterator. That is what this question is asking about. So C++20 seems like a very relevant tag.Interpretive
@Barry: But he specifically claims that "std::ostream_iterator can only be constructed with a stream." So he is explicitly not looking at C++20.Nealson
@Nicol Well, the claim is incorrect in 20. So the question becomes relevant... in 20.Interpretive
@NicolBolas: You realize you could have just edited my question the way I just edited it...Charlacharlady
I
12

As an update to this, P2325R3 was just adopted which makes std::ostream_iterator no longer default constructible (it was briefly made so in C++20).


This follows the Elements of Programming design philosophy of how types should behave. If you've heard the phrase "do as the ints do", that is that philosophy -- types should be Regular. And the EoP definition of Regular is:

T’s computational basis includes equality, assignment, destructor, default constructor, copy constructor, total ordering (or default total ordering) and underlying type

which translates to real C++20 concepts as:

template<class T>
  concept Movable = is_object_v<T> && MoveConstructible<T> && Assignable<T&, T>
    && Swappable<T>;
template<class T>
  concept Copyable = CopyConstructible<T> && Movable<T> && Assignable<T&, const T&>;
template<class T>
  concept Semiregular = Copyable<T> && DefaultConstructible<T>;
template<class T>
  concept Regular = Semiregular<T> && EqualityComparable<T>;

We've lost the total ordering part in favor of simply EqualityComparable, and even then a lot of the library requirements via Ranges actually only require Semiregular - not Regular. But still, this is the foundation of the idea.

Note that if a type is movable, it already kind of makes sense for it to be default constructible. The moved-from state is very conceptually similar to a default-constructed state. Can't do much from there, but it's a state.

Interpretive answered 10/5, 2019 at 23:2 Comment(9)
1. I think you're making a homuculus argument here. The question simply becomes: Why does it make sense for an ostream iterator to be regular? C++ type design trends doesn't subscribe to the notion that all types should be regular, or like the ints. 2. Interesting point about movability.Charlacharlady
@einpoklum: "C++ type design trends doesn't subscribe to the notion that all types should be regular, or like the ints." Where are you getting that from? A Herculean effort was made to ensure that variant could be default constructible if its first type was default constructible.Nealson
@NicolBolas: Note I said all types. So maybe for some types there's an attempt to strive for regularity, but it's just some.Charlacharlady
@einpoklum: If you're not dealing with a universal law that says "don't make something default constructible unless that state is meaningful", if you're willing to accept that "for some types" it's OK, then what's the problem with saying that all iterators shall be default constructible?Nealson
@NicolBolas: The problem is that it breaks the intuitive semantics of an iterator (well, my intuition of course). Having methods, especially constructors, which require a caveat saying "this doesn't really make sense but we have to kind of have to do it that way for a hack somewhere, please ignore it" - well, it makes me cringe a bit.Charlacharlady
Anyway, @Barry, +1 for the link to EoP.Charlacharlady
Forward-and-stronger iterators have always been required to be default constructible - but not necessarily to anything useful (see pointers). This change makes things more uniform.Sunburn
@T.C.: Perhaps that should be an answer.Charlacharlady
@T.C.: Also, why should they be default-constructible? Why have such a meaningless constructor?Charlacharlady
N
8

There are a lot of things in C++ where a non-default-constructible type is simply not workable. Here's a really simple example: extract a type T from an istream using the >> operator without default constructing T (or otherwise being given a live T). You can't, because the interface itself requires that one exists. The interface is designed to assume that you can always construct an object of an extractable type.

And if you're not given an object to work with, that means default constructing it.

This seems like a cherry picked example, but it isn't. It is a semi-frequent occurrence that in generic code, you sometimes need to just create a T so that you can fill bits of it in later.

However much we would like to say that objects should only be default constructible if it is meaningful for them to be in such a state, it simply is not a practical reality. Sometimes, you just have to create an object now and get it filled in with a useful value later.

As such, the Ranges v3 library enshrines this requirement in the basic and frequently used concept SemiRegular. That concept represents some of the more basic aspects of manipulation for objects: I can make one, and I can assign it. Iterators are required to follow that concept.


It should also be noted that, in C++20, ostream_iterator gains a default constructor.

Nealson answered 10/5, 2019 at 23:12 Comment(10)
I wonder even about your simple example. T has some sort of constructor, right? Well, you just need to construct its constructor's arguments, and so on. Eventually you're supposed to reach some set of default, character or string-constructible types; but it doesn't have to be T itself.Charlacharlady
The above notwithstanding - why do ostream (or similar) iterators absolutely have to be created "now" while waiting to be filled later?Charlacharlady
@einpoklum: But you don't know anything about T except that it is extractable. Because you're a template function.Nealson
If I don't know anything about T, obviously I should not be able extract it from an istream.Charlacharlady
@einpoklum: I said "except that it is extractable". Templates have requirements on their types, but they don't know (or need to know) everything about them.Nealson
An extractable T is a T with a constructor from type T' which is extractable by the same definition, or is directly extractable. So, if it's extractable we can indeed extract it without resorting to default-ctor silliness. Again, my intuition (and I'm ignoring the case of needing to parse multiple tokens for construction of a single value).Charlacharlady
@einpoklum: That is a very odd definition of extraction from a stream, one which has surprisingly little to do with the principle thing that an Extractable concept would be about: applying the expression istream >> t.Nealson
I can extract a foo from a bar if I can take stuff from the bar and make it into a foo. That's how my intuition goes, anyway.Charlacharlady
@einpoklum: You are clearly missing the point here. The example was istream extraction. Where you're getting this other stuff from, I don't know, but I thought I was really specific. That is a concrete example of a place where default constructibility is expected by the nature of the API, not the nature of T. If your reply is "well, I could rewrite iostreams to work a different way", then OK. But that doesn't change the fact that it exists that way right now. So please focus on that.Nealson
You're saying that I'm missing the point, but it's the C++ committee that has apparently been missing the point up until C++17. Default constructibility was not "expected by the nature of the API" so far - so is it really the nature of the API now?Charlacharlady

© 2022 - 2024 — McMap. All rights reserved.