1

I was wondering what went wrong in having a r-reference return type like:

vector<T>&& id2(vector<T>&& v) {  return std::move(v);}

as in the code below. Doing a = id2(a) sets empty. The other version id returning the normal type is OK.

I heard this has something to do with self-move assignment. But I only have a basic knowledge about move semantics, and I don't understand the cryptic language in the answer there quoted from the standards. Can someone please explain this in common-sense/layman's language?

If I take a temporary a, reap out its content and return the content as a temporary to be assigned back to a, why should a be cleared or undefined?

#include <vector>
#include <iostream>
using namespace std;

template <class T>
vector<T> id(vector<T>&& v) {  return std::move(v); }

template <class T>
vector<T>&& id2(vector<T>&& v) {  return std::move(v);}

int main() {
  vector<int> a;
  a.resize(3,1);
  a = id(std::move(a));
  cout << a.size() << endl;  //output: 3

  a.resize(3,1);
  a = id2(std::move(a));
  cout << a.size() << endl;  //output: 0
}

Thanks,

-- Update ---

If I am reading the said duplicate post, the second example there quoted below was about passing r-ref to a local variable outside a function, which is an error anyway regardless of this is a r-ref or l-ref. I don't think that's quite the same as my example. If you use left reference instead of r-ref in my example, it would have worked. Am I missing something on this?

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

--- Update 2 ---

@pradhan. It seems that the issue boils down to self-move assignment. Regarding the library choice of behavior on self-move assignment for standard containers, this is what bothers me. If you do a self-assign of std::vector in the old fashion, it's perfectly fine, but when you self-move assign, it's undefined behavior. Unless there is something inherently wrong, is it more natural (backward compatible) to have standard containers allow self-move assign as with self-assign?

#include <vector>
#include <iostream>

int main() {
  std::vector<int>a{1,2,3};
  a = a;
  std::cout << a.size() << std::endl;  //output: 3
  a = std::move(a);
  std::cout << a.size() << std::endl;  //output: 0
}
Community
  • 1
  • 1
thor
  • 21,418
  • 31
  • 87
  • 173
  • That's what I am asking, i.e. why? – thor Jul 07 '14 at 20:31
  • @juanchopanza, I don't think this is a duplicate of the said post. Please see the update. – thor Jul 07 '14 at 20:49
  • A more similar duplicate is [here](http://stackoverflow.com/questions/16226150). – Daniel Frey Jul 07 '14 at 20:51
  • I think the answer there says about range-based for loop and a quote from standards about it. It's also about a custom made string class. I don't think the answer in that post answers my question. Maybe – thor Jul 07 '14 at 20:59
  • 1
    Also, your code is equivalent to `a=std::move(a);` and that is not required to work (see @HowardHinnant's answer you linked). Your `id2` function does exactly nothing. – Daniel Frey Jul 07 '14 at 20:59
  • That's what am I asking exactly: why? (in layman's term). I mentioned there is related answer about self-move assignment as you pointed out. And I don't understand the cryptic standard-style language in the answer of the post (@HowardHinnant's answer) that I quoted. That's the only reason I ask, to understand why self-movement assign does nothing. – thor Jul 07 '14 at 21:02
  • 1
    To repeat the important part of Howard's answer: "So, the implementation of std::vector::operator=(vector&& other) is allowed to assume that other is a *prvalue*. And if other is a *prvalue*, self-move-assignment is not possible.". This does not mean that `a=std::move(a);` does nothing, it means the result of that operation is undefined, anything can happen. This was done by the standard to allow the move-assignment to skip the test for self-assignment and thus be more efficient. Once you understand that, go give Howard an upvote :) – Daniel Frey Jul 07 '14 at 21:06

3 Answers3

2

Your function id2 does nothing, the code is equivalent to

a = std::move(a);

The answer from Howard Hinnant you linked explains that this is in itself not allowed. In layman's terms: The standard does not require self-move assignment to be supported. Thus an implementation of a move-assignment operator is not required to perform a test for self-assignment and the result us undefined behavior.

If you would make a=std::move(a) legal, it would do nothing. The problem is that this requires a check which is superfluous for the non-self move-assignment case and therefore pessimize all those assignments.

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180
  • Thanks for the explanation. I understand the standards made some assumptions (seemingly out of nowhere), but those assumptions don't seem to make sense to me. What harm can it be if you allow a = std::move(a) to just swap a to a temporary and then back to itself? There still would be no self-copies, and `a` is still itself as if we did't use move semantics. – thor Jul 07 '14 at 21:17
  • That would be better IMHO to make a = std::move(a) illegal/undefined. – thor Jul 07 '14 at 21:17
  • @TingL It **is** illegal and undefined. It is just not detected by the compiler (which would be quite complicated) and neither will the generated code detect it - that is exactly what should be avoided. – Daniel Frey Jul 07 '14 at 21:18
  • @Ting: The problem is not with the swap... it's with the additional logic used to free the old value of the left-hand side. Move-assignment leaves the right-hand side in an unspecified state, but keeping the old content of the left (e.g. the prior value of `lhs` in `lhs = std::move(rhs);`) alive after it's been overassigned is a no-no. Think about move-assignment of `std::unique_ptr`, for example. The old stream needs to get closed. – Ben Voigt Jul 07 '14 at 21:18
2

You're violating the convention (which is a requirement for calls to library functions) that rvalue references do not alias. Howard Hinnant's answer, referred to in the question, contains the standard text and the application to self-move. Here's the practical result:

It's both legal and common to implement move-assignment as:

T& T::operator=(T&& other)
{
    clear();
    swap(*this, other);
    return *this;
}

or the more exception-safe variant:

T& T::operator=(T&& other)
{
    swap(*this, other);
    other.clear();
    return *this;
}

and as long as the aliasing convention is observed, this is also safe.

It's pretty clear why either of these implementations would yield an empty object if you do alias.

BTW, there's nothing wrong with id2 that returns an rvalue reference. The problem is with a = id2(a); which uses an rvalue reference that aliases another object reference in the same expression.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
0

The link from your question and Hinnant's answer both deal with the requirement placed by the standard on implementations of the standard library. There is nothing inherently wrong with a=std::move(a). If a is of class type T, the result of a=std::move(a) depends on the implementation of T. The standard allows the (standard)library implementations to make the unique reference assumption when given an rvalue reference argument. When a has type T=vector<U>, a=std::move(a) violates the unique reference assumption. Since you are breaking the library contract, it results in UB. However, you could have your own class whose move constructor implementation has a perfectly well-defined behaviour wrt a=std::move(a). The standard doesn't say anything about self-move assignment in general. It only specifies what the standard library implementors are required to support and what they can choose not to, if it pleases them.

Pradhan
  • 16,391
  • 3
  • 44
  • 59