Why does the constructor of ranges::iota_view not move the argument to member variable?

Viewed 217

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?

1 Answers

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.

Related