3

There's a similar question: check if elements of a range can be moved?

I don't think the answer in it is a nice solution. Actually, it requires partial specialization for all containers.


I made an attempt, but I'm not sure whether checking operator*() is enough.

// RangeType

using IteratorType = std::iterator_t<RangeType>;
using Type = decltype(*(std::declval<IteratorType>()));

constexpr bool canMove = std::is_rvalue_reference_v<Type>;

Update

The question may could be split into 2 parts:

  1. Could algorithms in STL like std::copy/std::uninitialized_copy actually avoid unnecessary deep copy when receiving elements of r-value?
  2. When receiving a range of r-value, how to check if it's a range adapter like std::ranges::subrange, or a container which holds the ownership of its elements like std::vector?
template <typename InRange, typename OutRange>
void func(InRange&& inRange, OutRange&& outRange) {
    using std::begin;
    using std::end;
    std::copy(begin(inRange), end(inRange), begin(outRange));
    // Q1: if `*begin(inRange)` returns a r-value,
    //     would move-assignment of element be called instead of a deep copy?
}

std::vector<int> vi;
std::list<int> li;
/* ... */
func(std::move(vi), li2);
// Q2: Would elements be shallow copy from vi?
// And if not, how could I implement just limited count of overloads, without overload for every containers?
// (define a concept (C++20) to describe those who take ownership of its elements)

Q1 is not a problem as @Nicol Bolas , @eerorika and @Davis Herring pointed out, and it's not what I puzzled about. (But I indeed think the API is confusing, std::assign/std::uninitialized_construct may be more ideal names)

@alfC has made a great answer about my question (Q2), and gives a pristine perspective. (move idiom for ranges with ownership of elements)


To sum up, for most of the current containers (especially those from STL), (and also every range adapter...), partial specialization/overload function for all of them is the only solution, e.g.:

template <typename Range>
void func(Range&& range) { /*...*/ }

template <typename T>
void func(std::vector<T>&& movableRange) {
    auto movedRange = std::ranges::subrange{
        std::make_move_iterator(movableRange.begin()),
        std::make_move_iterator(movableRange.end())
    };

    func(movedRange);
}

// and also for `std::list`, `std::array`, etc...
zjyhjqs
  • 609
  • 4
  • 11
  • 2
    The main thrust of the answer to that question is that you *don't need* to ask this question. If the user gives you a range of iterators that is moveable, then you will naturally move from them. The onus is on the user to provide a proper range. So... why do you feel like you need to ask this question? – Nicol Bolas May 22 '21 at 14:37
  • @NicolBolas If I don't know whether it's movable, how could I choose std::ranges::move/std::ranges::copy to avoid unnecessary copy? – zjyhjqs May 22 '21 at 14:55
  • 1
    That's not your job to decide. It should be the user's job to give you a moveable range. If you `std::move(container)` into some location, the implicit assumption is that the receiver is gaining ownership over the container itself, to the extent possible. If the user instead gives you a pair of `move_iterator`s over the container, then the assumption is that you're moving the elements, not the container itself. – Nicol Bolas May 22 '21 at 14:58
  • @NicolBolas But how to check `std::move_iterator` automatically? It's a template not a mere type, and template argument is unknown. – zjyhjqs May 22 '21 at 15:03
  • 1
    There is *nothing to check*. If the iterator is a move-iterator, then `*it` will be an xvalue. That's the whole point of the type: `T t = *it;` will *perform a move* if `it` is a move iterator. – Nicol Bolas May 22 '21 at 15:05
  • 1
    Checking whether something is movable, and checking if something is an rvalue reference are two entirely different things. – eerorika May 22 '21 at 16:08
  • @NicolBolas Assignment operator will work, but if I want to use STL algorithms? `move`/`copy` won't check for rvalue or not.. – zjyhjqs May 23 '21 at 05:56
  • @eerorika Isn't r-value type a suggestion of move semantics? Like what `std::move` does.. – zjyhjqs May 23 '21 at 06:01
  • 1
    @zjyhjqs There is no such thing as "r-value type" in C++. There is "r-value reference type", and there are r-value category of expressions. – eerorika May 23 '21 at 10:31
  • @eerorika Yes what I meant is 'r-value reference type'. Thanks for your correction, but what I emphasize is "a suggestion of move semantics". – zjyhjqs May 23 '21 at 14:57
  • 1
    @zjyhjqs Regardless, r-value reference doesn't mean that you can move. You can have an r-value reference to a non-movable type. And you can move from an l-value by converting it to an r-value reference. That's what I mean by *"Checking whether something is movable, and checking if something is an rvalue reference are two entirely different things."* – eerorika May 23 '21 at 15:03
  • @eerorika I understand, like the case that move-assignment is deleted. But without r-value-reference, we even don't know whether we *should* try to move.. – zjyhjqs May 24 '21 at 23:00
  • 1
    @zjyhjqs If you want to conditionally move, then are you perhaps looking for `std::forward`? – eerorika May 24 '21 at 23:02
  • @eerorika Back to my question, it's about how to check for a `range` which can be moved.. (Or with your suggestion, which is intended to be moved). It's not about an object but a `range` or `iterator`. – zjyhjqs May 24 '21 at 23:11
  • Perhaps show some code that doesn't do what you want (e.g. it makes a copy instead of moving things around), and then somebody will be able to tell what's wrong with it. – n. m. could be an AI May 18 '22 at 04:36
  • @n.1.8e9-where's-my-sharem. Question is updated – zjyhjqs May 18 '22 at 16:32
  • It isn't clear why you don't make a function that *returns* a `movedRange` and be done with it. – n. m. could be an AI May 18 '22 at 17:30
  • @n.1.8e9-where's-my-sharem. Then _the function returns a `movedRange`_ still need to overload for all of containers. – zjyhjqs May 19 '22 at 01:54
  • No, it needs to be a template that accepts a range. And you don't call it from inside of `func`, you pass the result to `func`. – n. m. could be an AI May 19 '22 at 03:51
  • @n.1.8e9-where's-my-sharem. So when to wrap with the `MovedRange` in `func`? That's the problem! The clients can only _**pass**_ a `MovedRange` into `func`, instead of a more intuitive way by `std::move`. – zjyhjqs May 19 '22 at 04:03
  • The algorithm cannot possibly know. Only the client knows. The client wraps a range: `func(move_elements(myrange))` instead of `func(std::move(myrange))`. It's the only way. A test scenario: you have a vector of 20 elements, you want to move out the first 10 in order, and move out the other 10 in reverse order. How do you do that? `func(move_elements(subrange(...)))` and then `func(move_elements(reverse(subrange(...))))`. – n. m. could be an AI May 19 '22 at 04:13
  • @n.1.8e9-where's-my-sharem. _Only the client knows_ is the problem. An additional utility tool like `MovedRange` increases learning cost and risk of misuse. And as what I updated, there's no perfect way currently. – zjyhjqs May 19 '22 at 05:54
  • A range does not necessarily own elements. If it is a container, sure, If it is a view, nope. You can have a view over a view over a view over an expiring container that can move its elements out, but the information about it being expiring is not automatically propagated through the chain of views, and there is no way to make it so. The user needs to learn how to do it manually. Perhaps a view over an expiring container could return move iterators as its begin() and end() but AFAICT this is not the case and was not ever considered. My guess is that this is too complicated/dangerous. – n. m. could be an AI May 19 '22 at 07:39
  • @n.1.8e9-where's-my-sharem. No, we could delete the iterator of an expiring container. See my answer. – zjyhjqs May 20 '22 at 13:59

3 Answers3

3

I understand your point. I do think that this is a real problem.

My answer is that the community has to agree exactly what it means to move nested objected (such as containers). In any case this needs the cooperation of the container implementors. And, in the case of standard containers, good specifications.

I am pessimistic that standard containers can be changed to "generalize" the meaning of "move", but that can't prevent new user defined containers from taking advantage of move-idioms. The problem is that nobody has studied this in depth as far as I know.

As it is now, std::move seem to imply "shallow" move (one level of moving of the top "value type"). In the sense that you can move the whole thing but not necessarily individual parts. This, in turn, makes useless to try to "std::move" non-owning ranges or ranges that offer pointer/iterator stability.

Some libraries, e.g. related to std::ranges simply reject r-value of references ranges which I think it is only kicking the can.

Suppose you have a container Bag. What should std::move(bag)[0] and std::move(bag).begin() return? It is really up to the implementation of the container decide what to return.

It is hard to think of general data structures, bit if the data structure is simple (e.g. dynamic arrays) for consistency with structs (std::move(s).field) std::move(bag)[0] should be the same as std::move(bag[0]) however the standard strongly disagrees with me already here: https://en.cppreference.com/w/cpp/container/vector/operator_at And it is possible that it is too late to change.

Same goes for std::move(bag).begin() which, using my logic, should return a move_iterator (or something of the like that).

To make things worst, std::array<T, N> works how I would expect (std::move(arr[0]) equivalent to std::move(arr)[0]). However std::move(arr).begin() is a simple pointer so it looses the "forwarding/move" information! It is a mess.

So, yes, to answer your question, you can check if using Type = decltype(*std::forward<Bag>(bag).begin()); is an r-value but more often than not it will not implemented as r-value. That is, you have to hope for the best and trust that .begin and * are implemented in a very specific way.

You are in better shape by inspecting (somehow) the category of the range itself. That is, currently you are left to your own devices: if you know that bag is bound to an r-value and the type is conceptually an "owning" value, you currently have to do the dance of using std::make_move_iterator.

I am currently experimenting a lot with custom containers that I have. https://gitlab.com/correaa/boost-multi However, by trying to allow for this, I break behavior expected for standard containers regarding move. Also once you are in the realm of non-owning ranges, you have to make iterators movable by "hand".

I found empirically useful to distinguish top-level move(std::move) and element wise move (e.g. bag.mbegin() or bag.moved().begin()). Otherwise I find my self overloading std::move which should be last resort if anything at all.

In other words, in

template<class MyRange>
void f(MyRange&& r) {
   std::copy(std::forward<MyRange>(r).begin(), ..., ...);
}

the fact that r is bound to an r-value doesn't necessarily mean that the elements can be moved, because MyRange can simply be a non-owning view of a larger container that was "just" generated.

Therefore in general you need an external mechanism to detect if MyRange owns the values or not, and not just detecting the "value category" of *std::forward<MyRange>(r).begin() as you propose.

I guess with ranges one can hope in the future to indicate deep moves with some kind of adaptor-like thing "std::ranges::moved_range" or use the 3-argument std::move.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • I tried to define the behaviour of `std::move(Bag)[0]` and `std::move(Bag).begin()`. See my answer and hope some advices. – zjyhjqs May 20 '22 at 14:02
0

If the question is whether to use std::move or std::copy (or the ranges:: equivalents), the answer is simple: always use copy. If the range given to you has rvalue elements (i.e., its ranges::range_reference_t is either kind(!) of rvalue), you will move from them anyway (so long as the destination supports move assignment).

move is a convenience for when you own the range and decide to move from its elements.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • If you can detect that the the source is bound to an r-value you are better off using some kind of move explicitly (e.g. `template void transfer_to(std::vector&& v, DestContainerNonVector& dest) { assert(v.size() == dest.size()); std::move(v.begin(), v.end(), dest.begin());}`). You say always and you make the exception at the end. This is the case that OP wants to handle generically. The points is that this is difficult to generalize if you don't know the source range or container. – alfC May 18 '22 at 06:44
  • @alfC: That’s not an exception, since a range you own isn’t an unknown that needs checking: put differently, there’s no *detection* in `transfer_to`. If, as a convenience, you want to automatically move from the elements of a range argument that happens to be an rvalue **container**, that’s a different question. – Davis Herring May 18 '22 at 14:48
0

The answer of the question is: IMPOSSIBLE. At least for the current containers of STL.


Assume if we could add some limitations for Container Requirements?

Add a static constant isContainer, and make a RangeTraits. This may work well, but not an elegant solution I want.

Inspired by @alfC , I'm considering the proper behaviour of a r-value container itself, which may help for making a concept (C++20).

There is an approach to distinguish the difference between a container and range adapter, actually, though it cannot be detected due to the defect in current implementation, but not of the syntax design.


First of all, lifetime of elements cannot exceed its container, and is unrelated with a range adapter.

That means, retrieving an element's address (by iterator or reference) from a r-value container, is a wrong behaviour.


One thing is often neglected in post-11 epoch, ref-qualifier.

Lots of existing member functions, like std::vector::swap, should be marked as l-value qualified:

auto getVec() -> std::vector<int>;

//
std::vector<int> vi1;
//getVec().swap(vi1); // pre-11 grammar, should be deprecated now
vi1 = getVec(); // move-assignment since C++11

For the reasons of compatibility, however, it hasn't been adopted. (It's much more confusing the ref-qualifier hasn't been widely applied to newly-built ones like std::array and std::forward_list..)


e.g., it's easy to implement the subscript operator as we expected:

template <typename T>
class MyArray {
    T* _items;
    size_t _size;
    /* ... */

public:
    T& operator [](size_t index) & {
        return _items[index];
    }
    const T& operator [](size_t index) const& {
        return _items[index];
    }

    T operator [](size_t index) && {
        // not return by `T&&` !!!
        return std::move(_items[index]);
    }

    // or use `deducing this` since C++23
};

Ok, then std::move(container)[index] would return the same result as std::move(container[index]) (not exactly, may increase an additional move operation overhead), which is convenient when we try to forward a container.

However, how about begin and end?


template <typename T>
class MyArray {
    T* _items;
    size_t _size;
    /* ... */

    class iterator;
    class const_iterator;
    using move_iterator = std::move_iterator<iterator>;

public:
    iterator begin() & { /*...*/ }
    const_iterator begin() const& { /*...*/ }

    // may works well with x-value, but pr-value?
    move_iterator begin() && {
        return std::make_move_iterator(begin());
    }

    // or more directly, using ADL
};

So simple, like that?

No! Iterator will be invalidated after destruction of container. So deferencing an iterator from a temporary (pr-value) is undefined behaviour!!

auto getVec() -> std::vector<int>;

///
auto it = getVec().begin(); // Noooo
auto item = *it; // undefined behaviour

Since there's no way (for programmer) to recognize whether an object is pr-value or x-value (both will be duduced into T), retrieving iterator from a r-value container should be forbidden.


If we could regulate behaviours of Container, explicitly delete the function that obtain iterator from a r-value container, then it's possible to detect it out.

A simple demo is here: https://godbolt.org/z/4zeMG745f


From my perspective, banning such an obviously wrong behaviour may not be so destructive that lead well-implemented old projects failing to compile.

Actually, it just requires some lines of modification for each container, and add proper constraints or overloads for range access utilities like std::begin/std::ranges::begin.

zjyhjqs
  • 609
  • 4
  • 11
  • I agree with the aim and the method. but I have a couple of differences in the technique and philosophy. This is what i did in my own experiments. First I have `T&& operator[](size_t i) && {return std::move(operator[](i));}`. This way it is more consistent AND you save the move. Also more consistently I would leave .begin() as you initially proposed (and rejected later). Why? because it is more consistent with (my) op[] and second you can still use begin and end in one lines. `copy(move(v).begin(), move(v).end(), dest)`. if we convene that semantically that begin nor end can shallow-move `v`. – alfC May 20 '22 at 17:41
  • @alfC The keypoint is _saving address of element from a r-value container is dangerous_. When clients notice a function returning reference type `T&&`, they may tend to use universal reference `auto&&`/`const auto&`, which would lead to a dangling pointer problem when the container is a temporary like `getVec()`. Same as the iterator. – zjyhjqs May 21 '22 at 03:39
  • well, we fundamentally disagree here. the problem is precisely trying to save the address of something that is about to expire, if you just use on the fly it is ok. and, on my argument, the same goes for the reference. iterators and references can always dangle, you can’t protect against that. in what i am proposing is to make clear what is about to expire. – alfC May 21 '22 at 03:54
  • @alfC In my opinion, with well-implemented utilities, clients don't need to take iterators from an expiring container. Just like `copy(std::move(v), dest)` or `copy(MovedRange{v.begin() + 2, v.end() - 3}, dest)` is ok. Those who implement **`copy`** could do proper optimization with sufficient information when receiving a r-value. – zjyhjqs May 21 '22 at 06:29
  • In an ideal world yes, however *somebody* still has to implement the operations on expiring containers and for that he/she will use iterators (or ranges which have the same problems). Also, in reality, expiring or not, iterators(/ranges) are still the glue between containers and algorithms. Look in my library, I implemented `operator[]` and `begin/end` the way I propose and this allow this code: https://gitlab.com/correaa/boost-multi/-/blob/master/test/element_access.cpp#L338-363 . The goal of a library is not to protect users from bad behavior, it is to allow them to do powerful stuff. – alfC May 23 '22 at 20:27
  • If you want, let's continue on chat or in the issues here: https://gitlab.com/correaa/boost-multi/-/issues – alfC May 23 '22 at 20:27