9

What is generally the proper way to forward an std::unique_ptr?

The following code uses std::move, which I thought was the considered practice, but it crashes with clang.

class C {
   int x;
}

unique_ptr<C> transform1(unique_ptr<C> p) {
    return transform2(move(p), p->x); // <--- Oops! could access p after move construction takes place, compiler-dependant
}

unique_ptr<C> transform2(unique_ptr<C> p, int val) {
    p->x *= val;
    return p;
}

Is there a more robust convention than simply making sure you get everything you need from p before transferring ownership to the next function via std::move? It seems to me using move on an object and accessing it to provide a parameter to the same function call could be a common mistake to make.

Danra
  • 9,546
  • 5
  • 59
  • 117
  • 3
    Have you ever considered using raw pointers? It's these situations where so-called 'smart pointers' are extremely impractical for use. – Poriferous Jan 27 '16 at 16:19
  • 4
    This code definitely shouldn’t crash, at least not for the reason you mention — `std::move` doesn’t actually perform a move, and `p->x` does *not* access `p` after the move. – Konrad Rudolph Jan 27 '16 at 16:19
  • 14
    @KonradRudolph: It could still fail. The initialization of the parameter to `transform2` *will* perform move construction. At which point, `p` in `transform1` is empty. Remember: compilers can re-order these expressions any way they see fit. – Nicol Bolas Jan 27 '16 at 16:21
  • @NicolBolas Expression reordering alone cannot cause the crash because, as mentioned, the expression `std::move(p)` does nothing, it just changes the static type of the expression. That said, you might be right because I don’t know at what exact time point parameter initialisation happens; I *thought* it happened after all parameter expressions were evaluated, but I have no idea. – Konrad Rudolph Jan 27 '16 at 16:24
  • Why pass `p->x` separately when you're passing the whole object anyway? – Jonathan Potter Jan 27 '16 at 16:35
  • 1
    @JonathanPotter: Presumably, `transform2` could be used with a value other than `p->x`; `transform2` is general purpose multiplication, `transform1` is a specialization that squares. – ShadowRanger Jan 27 '16 at 16:37
  • @KonradRudolph you are right, order of evaluation is not specified in the standard, see http://stackoverflow.com/questions/2934904/order-of-evaluation-in-c-function-parameters thus using move at any point of the program will cause any use of that object later on to activate UB. – user1708860 Jan 27 '16 at 16:41
  • If you are not transferring ownership, you should not be passing a smart pointer. Anyway, Nicol Bolas nailed it. – T.C. Jan 27 '16 at 16:41
  • @user1708860 I am actually saying the exact opposite: the mere use of `std::move` does *not* cause later uses of that object to be UB. On the contrary, the following code is completely fine according to the standard: `auto&& q = std::move(p); p->x;`. You have a misconception about what `std::move` does. – Konrad Rudolph Jan 27 '16 at 16:50
  • @KonradRudolph, after looking it up, i see what you mean. The code is indeed fine, as is - it will not activate UB. If i understand correctly it will activate unspecified behavior, which will not cause a crash, but it is not promised to give a correct result. – user1708860 Jan 27 '16 at 17:02
  • @user1708860 The code I’ve shown in my comment is well-defined: it gives neither undefined nor unspecified behaviour, and it always gives the correct result, guaranteed. However, as for the code in OP’s case: see Nicol’s comment above (and my reply to that). – Konrad Rudolph Jan 27 '16 at 17:05
  • @KonradRudolph can you quote the standard or some other resource? – user1708860 Jan 27 '16 at 17:14
  • @user1708860 http://en.cppreference.com/w/cpp/utility/move is sufficient — look at the section “Return value” in particular. That’s all `std::move` does! – Konrad Rudolph Jan 27 '16 at 17:41
  • 3
    What would be nice is a compiler that warns about possible use of moved-from objects, in the same way as for uninitialized variables. – Toby Speight Jan 27 '16 at 17:49
  • The comment in the code in the question is a bit misleading. The issue is that you could access `p` after the move construction of the function parameter takes place. – David Schwartz Jan 27 '16 at 18:31
  • @DavidSchwartz Improved comment, thanks – Danra Jan 27 '16 at 18:36
  • @NicolBolas Are the expressions fully evaluated before any parameter initialization occurs, or can the expressions *and parameter initialization* be reordered? I suspect initialization can be reordered relative to each other and the expressions, but Konrad's point is that expression reordering is insufficient, I think. – Yakk - Adam Nevraumont Jan 27 '16 at 18:53
  • @Yakk: "*can the expressions and parameter initialization be reordered*" I don't know. Generally speaking, when esoteric rules like reording are involved, I err on the side of caution. – Nicol Bolas Jan 27 '16 at 19:28

6 Answers6

5

Since you do not really need to access p after it was moved, one way would be to get p->x before the move and then use it.

Example:

unique_ptr<C> transform1(unique_ptr<C> p) {
    int x = p->x;
    return transform2(move(p), x);
}
vincentleest
  • 925
  • 1
  • 8
  • 18
SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • Yes, but the question is "Is there a *more robust convention* than simply making sure you get everything you need from p before transferring ownership to the next function via std::move", which is what you suggest. The idea is to prevent this bug in general. – Danra Jan 27 '16 at 16:43
  • 2
    @Danra, but you can't. You can not use contents of the pointer after it was moved, and (as outlined in the comments) if you use `std::move` in the function calling context it is undefined when the object will be moved. In a single-threaded program you can get a raw pointer and use it in the function arguments, though. Is this what you are looking to get? – SergeyA Jan 27 '16 at 16:54
  • No, I'm looking for a way which could prevent this seemingly (to me at least) easy bug to make. e.g. Some sort of move-like operation but which would only invalidate the object after it goes out of scope (and before its destructor is called) – Danra Jan 27 '16 at 16:59
  • But that's just an example, I'm open to any other robust solution, if one exists – Danra Jan 27 '16 at 17:01
  • 1
    @Danra, this is not unique_ptr does! It invalidates the object right after the move, because it is very rationale for it's existance. If the object would be invalidated only when it goes out of scope, you would end up with more than one owner (the one which is still in-scope, and the one you've just passed it to). What you are saying seems to be use-case for shared_ptr, not unique_ptr. – SergeyA Jan 27 '16 at 17:03
  • @Danra if you are looking for a dummy proof way of doing this, you should look for something more like David Haim's answer and remove the ownership logic. – user1708860 Jan 27 '16 at 17:04
  • @user1708860, i totally disagree with what David is saying. He is effectively advocating agains move semantics at all. – SergeyA Jan 27 '16 at 17:05
  • @SergeyA Not really, shared_ptr offers more semantics such as copying which increments its counter. The requirement of staying 'alive' until going out of scope is a weaker one than what shared_ptr provides. – Danra Jan 27 '16 at 17:10
  • @user1708860 This is a simplified example. Assume move semantics are indeed required. The problem presented is a general one. – Danra Jan 27 '16 at 17:14
4

The code is not fine.

  • std::move is nothing but a cast (g++: something like static_cast<typename std::remove_reference<T>::type&&>(value))
  • Value computation and side effects of each argument expression are sequenced before execution of the called function.
  • However, the initialization of function parameters takes place in the context of the calling function. (Thanks to T.C)

Quotes from the draft N4296:

1.9/15 Program execution

[...] When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function. [...]

5.2.2/4 Function call

When a function is called, each parameter (8.3.5) shall be initialized (8.5, 12.8, 12.1) with its corresponding argument. [ Note: Such initializations are indeterminately sequenced with respect to each other (1.9) end note ] [...] The initialization and destruction of each parameter occurs within the context of the calling function. [...]

A sample (g++ 4.8.4):

#include <iostream>
#include <memory>
struct X
{
    int x = 1;
    X() {}
    X(const X&) = delete;
    X(X&&) {}
    X& operator = (const X&) = delete;
    X& operator = (X&&) = delete;
};


void f(std::shared_ptr<X> a, X* ap, X* bp, std::shared_ptr<X> b){
    std::cout << a->x << ", " << ap << ", " << bp << ", " << b->x << '\n';
}

int main()
{
    auto a = std::make_unique<X>();
    auto b = std::make_unique<X>();
    f(std::move(a), a.get(), b.get(), std::move(b));
}

The output may be 1, 0xb0f010, 0, 2, showing a (zero) pointer moved away.

Community
  • 1
  • 1
  • 5
    "initialization of function parameters" is not a "expression or statement in the body of the called function". In fact, your own quote says that it "occurs within the context of the calling function", not the called function. – T.C. Jan 27 '16 at 17:42
  • Note the possible output should read `1, 0xb0f010, 0, 1`, to match the edited code. You can't make edits of 1 character so this comment serves as a sanity check to anyone reading. – Mike Lui Dec 29 '18 at 03:59
3

Stop doing more than one thing on a single line, unless the operations are non-mutating.

Code isn't faster if it is all on one line, and it is often less correct.

std::move is a mutating operation (or more accurately, it flags an operation to follow as "mutate ok"). It should be on its own line, or at the least it should be on a line with no other interaction with its parameter.

This is like foo( i++, i ). You modified something and also used it.

If you want a universal brainless habit, simply bind all arguments in std::forward_as_tuple, and call std::apply to call the function.

unique_ptr<C> transform1(unique_ptr<C> p) {
  return std::experimental::apply( transform2, std::forward_as_tuple( std::move(p), p->x ) );
}

which avoids the problem because the mutation of p is done on a line different than the reading of the p.get() address in p->x.

Or, roll your own:

template<class F, class...Args>
auto call( F&& f, Args&&...args )
->std::result_of_t<F(Args...)>
{
  return std::forward<F>(f)(std::forward_as_tuple(std::forward<Args>)...);
}
unique_ptr<C> transform1(unique_ptr<C> p) {
  return call( transform2, std::move(p), p->x );
}

The goal here is to order the evaluation-of-parameter expressions separately from the evaluation-of-parameter-initialization. It still doesn't fix some move-based issues (like SSO std::basic_string move-guts and reference-to-char issues).

Next to that, hope that a compiler will add warnings for unordered mutate-and-read in the general case.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • The first solution seems a bit naive to me; rvalue-reference parameters are widespread since c++11, and as soon as you chain more than one such function passing a parameter through the chain, you are at risk of getting the above bug, and it *looks* fine to just one-line the entire forwarding to the next function in the chain, including the move. The second solution is nice but not something easily accessible to everyone, to prevent them from tripping over the potential bug. – Danra Jan 27 '16 at 19:06
  • @Danra Forwarding, without accessing data, is as safe as the caller was: if they didn't flag a mutation at the same time as a read, you should be fine. Forwarding, with additional data access, requires scrutiny that the thing you are forwarding isn't the thing you are reading elsewhere (as a forward is a possible move)? Can you construct an explicit counter example? – Yakk - Adam Nevraumont Jan 27 '16 at 19:15
  • Explicit counter example to what? Not sure I gotcha. – Danra Jan 27 '16 at 19:24
  • @Danra My claim is that forwarding won't introduce such errors, because the error will have occurred at the point where the forwarding was *started*. On the other hand, you said that chained forwards somehow increase the risk of the above bug. Is there an example where this happens deep in a chain, but not obviously at the interface where the move was done? – Yakk - Adam Nevraumont Jan 27 '16 at 20:23
  • First two sentences "Stop doing more than one thing on a single line, unless the operations are non-mutating. Code isn't faster if it is all on one line, and it is often less correct." of this answer are really the crux of the issue imo. The other answers are beating around the bush. – Chris Beck Jan 28 '16 at 19:07
  • @Yakk I just meant that the offending function (transform1 in my example) might itself have gotten its parameter through a move constructor, and this could become more common as forwarding becomes more commonplace in code: it's reasonable there will be a few functions in a chain forwarding parameters one to the next, with one of the functions in the middle doing a mistake such as in transform1. – Danra Jan 31 '16 at 11:32
3

As noted in Dieter Lücking's answer, the value computations are sequenced before function body, so the std::move and operator -> are sequenced before the body of the function --- 1.9/15.

However, this does not specify that the parameter initialization is done after all of those computations, they can appear anywhere with regard to each other, and to non-dependent value computations, as long as they are done before the function body --- 5.2.2/4.

This means the behaviour is undefined here, as one expression modifies p (moving into a temporary argument) and the other uses the value of p, see https://stackoverflow.com/a/26911480/166389. Although as mentioned there, P0145 proposes to fix the evaluation order to left-to-right (in this case). Which would mean your code is broken, but transform2(p->x, move(p)) would do what you want. (Corrected, thanks to T.C.)

As far as idioms go to avoid this, consider David Haim's approach taking the unique_ptr<C> by reference, although that's pretty opaque to the caller. You're signalling something like "May modify this pointer". unique_ptr's moved-from state is reasonably clear, so this isn't likely to bite you as badly as if you move from a passed-in object reference or something.

In the end, you need a sequence point between using p and modifying p.

Community
  • 1
  • 1
TBBle
  • 1,436
  • 10
  • 27
2

Generally speaking, no, there isn't a fool-proof method of doing this without taking care of the small details. Where it gets really sneaky is in member-initialization-lists.

Just an as example that goes one step beyond yours, what happens if p->x is itself an object whose lifetime depends on *p, and then transform2(), which is effectively unaware of the temporal relationship between its arguments, passes val onwards to some sink function, without taking care to keep *p alive. And, given its own scope, how would it know it should?

Move semantics are just one of a set of features that can be easily abused, and needs to be handled with care. Then again, that's part of the essential charm of C++: it requires attention to details. In return for that added responsibility, it gives you more control—or is it the other way around?

Yam Marcovic
  • 7,953
  • 1
  • 28
  • 38
  • Once the move construction takes place, anything inside p whether a POD or an object would be invalid, so I'm not sure I got the gist of your extended example. Regarding the philosophical part, I feel the demonstrated bug could be a common gotcha, so I hoped there is a more robust way to avoid it. Given that there isn't one currently, I hope one could be added to std. – Danra Jan 27 '16 at 19:19
  • @Danra The point is that even if you had taken care to fetch `p->x` _prior_ to issuing the call, then, depending on what `p->x` _is_, you might still encounter bugs. It just went to show that there's a lot of details that need to be taken into consideration to write correct C++. I'd say that `std::move()` isn't a "do-always" kind of thing. Rather, it should be used when _necessary_ to achieve correctness (first), then performance. It's an inherently dangerous tool, since it allows you to retain access to invalid objects. – Yam Marcovic Jan 27 '16 at 19:23
  • I see your point now. But copying/moving p->x beforehand, and still having its lifetime depend on *p, sounds to me like you've designed something wrong :) However, copying p->x into a parameter of a function call, at the same time as you're moving p itself into another parameter of the function call, sounds like it should work - or at least looks like it should, in a one-liner. – Danra Jan 27 '16 at 19:31
1

ammm... not much of an answer but a suggestion - why pass the ownership of the unique_ptr in the first place? it seems that transformXXX play with the integer value, why does memory management has to play a part here?

pass the unique_ptr by reference :

unique_ptr<C>& transform1(unique_ptr<C>& p) {
    return transform2(p, p->x); // 
}

unique_ptr<C>& transform2(unique_ptr<C> p&, int val) {
    p->x *= val;
    return p;
}

pass ownership outside these functions. create specific functions which this is their job. seperate algebric logic from memory management.

David Haim
  • 25,446
  • 3
  • 44
  • 78
  • Indeed, it's a simplified example. In my real case I do need to transfer ownership. – Danra Jan 27 '16 at 16:32
  • your functions shouldn't do more that one good thing. passing owneship and doing math is a job for two functions, not one. – David Haim Jan 27 '16 at 16:33
  • 1
    @DavidHaim, if what you are saying would be true, there would be no need for any ownership semantics within any smart pointer. – SergeyA Jan 27 '16 at 16:35
  • @SergeyA no, I'm saying that there should be functions which pass memory, and functions that do math, not both. – David Haim Jan 27 '16 at 16:36
  • This should just pass a non-owning raw pointer (or an `observer_ptr`). If you are not touching ownership, you shouldn't care what owns the object. – T.C. Jan 27 '16 at 17:44