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?
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.
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 int
s 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With