4

In [range.iota.view], the well-known constructor of iota_view is defined as follows (emphasis mine):

constexpr explicit iota_view(W value);

Preconditions: Bound denotes unreachable_­sentinel_­t or Bound() is reachable from value.

Effects: Initializes value_­ with value.

Why does the constructor use value to initialize value_ instead of std::move(value)? In my opinion, it seems to be more efficient to use std::move(value) since there may be memory allocation inside type W, which may lead to the overhead of copy construction.

In addition, other range adapters such as filter_view or transform_view and their iterators also directly move the accepted arguments into member variables during construction, so there seems to be an inconsistency here.

So why does iota_view copy-construct its member variables instead of move-construct? What are the considerations behind this?

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • At a glance, it doesn't look like copy-construction is required, just that move-construction is not a hard requirement. The few implementations I looked at all use `std::move()`. –  Jun 28 '21 at 17:15
  • 2
    I'm not *quite* confident enough to make this an answer but: `iota_view` requires `W` to be `copyable` whereas `transform_view` and `filter_view` require `V` to be `movable`, so the language in the constructors simply reflects this. For `iota`, copy-vs-move is an implementation detail. For the others, there is no choice. –  Jun 28 '21 at 17:33

1 Answers1

1

Emboldened by two upvotes and no-one contradicting me, here's my comment to OP in a more verbose form.

So why does iota_view copy-construct its member variables instead of move-construct?

That's not what is being implied here.

Initializes value_­ with value.

This only means that value, the name of the constructor's argument, is used to initialize value_, the member. It does not say anything about how that happens.

That's how the spec for the standard library works for the most part: it enforces only the strict minimum to ensure the desired semantics and behaviour, providing implementors with as much leeway as possible. There's nothing stopping iota_view from using move-semantics, there's just no obligation to do so.

A quick look at MSVC's implementation shows that it does use move construction:

        constexpr explicit iota_view(_Wi _Value_) noexcept(
            is_nothrow_move_constructible_v<_Wi>&& is_nothrow_default_constructible_v<_Bo>) // strengthened
            : _Value(_STD move(_Value_)) {}

Now, the obvious question becomes: "Why does filter_view and transform_view specify that std::move() must be used?"

N.B. The following is speculation, based on the assumption that the distinction is intentional, and not an oversight one way or another.

While there might be some other reason why the wording was chosen to be so strict, I believe the difference between the class declarations gives us a reasonable explanation for the apparent inconsistency:

template<weakly_­incrementable W, semiregular Bound = unreachable_sentinel_t>
    requires weakly-equality-comparable-with<W, Bound> && copyable<W>
  class iota_view : public view_interface<iota_view<W, Bound>> {...};

template<input_­range V, indirect_­unary_­predicate<iterator_t<V>> Pred>
    requires view<V> && is_object_v<Pred>
  class filter_view : public view_interface<filter_view<V, Pred>> {...};

template<input_­range V, copy_­constructible F>
    requires view<V> && is_object_v<F> &&
             regular_­invocable<F&, range_reference_t<V>> &&
             can-reference<invoke_result_t<F&, range_reference_t<V>>>
  class transform_view : public view_interface<transform_view<V, F>> {

Where view is:

template<class T>
  concept view =
    range<T> && movable<T> && enable_view<T>;

The big difference is that iota_view's W is a copyable<>, whereas filter_view and transform_view only expect a movable<> for V. So these later ones must be written in a way that accommodates move-only types (like std::unique_ptr<>).

Since iota_view has to work with types matching the copyable<> concept, either copy-construction or move-construction would be acceptable from a semantics point of view. The decision is left to the implementor, even if there's no apparent reason to not use move-construction.

  • As a quick addendum, I do find it a bit redundant that the wording for `filter_view` and `transform_view` is so strict. The constraint is already imposed by the use of the `movable<>` concept. There's no harm done though, so it's not exactly a big deal. –  Jun 29 '21 at 11:52
  • Hey, Frank, thanks for a look into this, upvote. But `transform_view` requires `F` must satisfy `copy_constructible`, which means it is still well-formed without using `std::move`, can you explain why the standard [here](http://eel.is/c++draft/range.transform.view#1) specifies `std::move`? – 康桓瑋 Jun 29 '21 at 15:11
  • @康桓瑋 Good catch, I've been focusing so much on `V` that I missed that. I'm sure there's a reason, but I can't come up with a satisfying one at the moment. –  Jun 29 '21 at 15:28