4

I have a segment similar to the following.

struct derive : base{
    derive(unique_ptr ptr): base{func(ptr->some_data), std::move(ptr)}{}
};

In theory, it should work. But since the compiler (vs2015) does not strictly follow the standard, the order of func(ptr->some_data), std::move(ptr) is undefined, i.e. ptr may be moved before accessed.

So my problem is how to make this segment work as expected?

Complete code like this:

#include <memory>

struct base {
    virtual ~base() = 0 {}

protected:
    base(std::unique_ptr<base> new_state) :
        previous_state{ std::move(new_state) } {}
private:
    std::unique_ptr<base> previous_state;
};

struct derive_base : base {
    int get_a() const noexcept {
        return a;
    }
protected:
    derive_base(int const new_a, std::unique_ptr<base> new_state) :
        base{ std::move(new_state) }, a{ new_a } {}
private:
    int a;
};

struct final_state : derive_base {
    final_state(std::unique_ptr<base> new_state) :
        derive_base{ dynamic_cast<derive_base&>(*new_state).get_a(), std::move(new_state) } {}
};
cqdjyy01234
  • 1,180
  • 10
  • 20
  • 3
    Did you have a question? – Ben Voigt Feb 05 '15 at 04:27
  • I saw one just a second ago... honest. the last edit wiped it. – WhozCraig Feb 05 '15 at 04:28
  • @WhozCraig Yes. My parenthesis stack got corrupted. – Drew Dormann Feb 05 '15 at 04:32
  • And what does the `base` constructor look like? Depending on that this call may already be safe. – Praetorian Feb 05 '15 at 04:32
  • @DrewDormann: I read it that way first as well – Ben Voigt Feb 05 '15 at 04:33
  • @Praetorian: No, it's safe regardless of the `base::base()` implementation, barring very serious compiler bugs (far worse than Visual C++'s usual level of noncompliance) – Ben Voigt Feb 05 '15 at 04:33
  • @BenVoigt How so? If `base` is also taking the `unique_ptr` by value, and VS2015 does not ensure left-right evaluation for list initialization, then the pointer might get moved before the first argument is evaluated. That won't be the case if the parameter type is `unique_ptr&&` – Praetorian Feb 05 '15 at 04:35
  • @DrewDormann not a day goes by mine doesn't do the same at some point. – WhozCraig Feb 05 '15 at 04:35
  • @Praetorian Yes, it may be if the second parameter is used following the first. But it can be any order in general. – cqdjyy01234 Feb 05 '15 at 04:36
  • @Praetorian: Oh so you're thinking there may be an issue if there is NO base constructor at all? Not what your comment said. I have to think about this case some more... – Ben Voigt Feb 05 '15 at 04:36
  • 1
    @BenVoigt Now I'm confused (an SSCCE would avoid all of this!). I was thinking of the case where the `base` constructor in question is `base(result_of_func_type, unique_ptr)` as opposed to `base(result_of_func_type, unique_ptr&&)`. If VS is non-compliant in order of evaluation the former is not safe but the latter is. – Praetorian Feb 05 '15 at 04:38
  • @Praetorian: And aggregate initialization could behave still differently (but the new comment concerning order of declaration rules this out) – Ben Voigt Feb 05 '15 at 04:44

2 Answers2

7

You can fix it using constructor chaining:

struct derive : base
{
  private:
    derive(const D& some_data, unique_ptr<X>&& ptr) : base{some_data, std::move(ptr)} {}
  public:
    derive(unique_ptr<X> ptr): derive(func(ptr->some_data), std::move(ptr)) {}
};

Reason: As explained in my other answer, the call to func definitely takes place before the delegated constructor call, while actually moving the unique_ptr (as opposed to merely changing its value category) definitely takes place inside.

Of course, this relies on another C++11 feature which Visual C++ may or may not have gotten right. Happily, delegating constructors are listed as supported since VS2013.


An even better thing to do is just always accept std::unique_ptr arguments by reference, and by rvalue reference if you plan to steal from them. (And if you won't steal the content, why do you care what type of smart pointer the caller has? Just accept a raw T*.)

If you used

struct base
{
    virtual ~base() = 0 {}

protected:
    base(std::unique_ptr<base>&& new_state) :
        previous_state{ std::move(new_state) } {}
private:
    std::unique_ptr<base> previous_state;
};

struct derive_base : base
{
    int get_a() const noexcept {
        return a;
    }
protected:
    derive_base(int const new_a, std::unique_ptr<base>&& new_state) :
        base{ std::move(new_state) }, a{ new_a } {}
private:
    int a;
};

struct final_state : derive_base
{
    final_state(std::unique_ptr<base>&& new_state) :
        derive_base{ dynamic_cast<derive_base&>(*new_state).get_a(), std::move(new_state) } {}
};

you wouldn't have had the problem in the first place, and the caller requirements are completely unchanged (an rvalue must be provided, since unique_ptr is uncopyable anyway)


The rationale for making this a universal rule is as follows: pass by value allows either copying or moving, whichever is more optimal at the call site. But std::unique_ptr is non-copyable, so the actual parameter MUST be an rvalue anyway.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • It is a solution. I will accept this without better solutions. – cqdjyy01234 Feb 05 '15 at 04:51
  • With your new edit, I would wonder whether it is better to accept `unique_ptr` by rvalue reference in general? – cqdjyy01234 Feb 05 '15 at 04:55
  • @user1535111: Yes, if you plan to steal from them. Otherwise use `const unique_ptr&`. That's why I said "always". – Ben Voigt Feb 05 '15 at 04:55
  • 1
    You already got a +1 from me for the first half, but I disagree with the second. I'm of the opinion that you should take the `unique_ptr` by value if you plan on taking ownership, taking by rvalue ref leaves it ambigous whether you actually moved from it or not. See NicolBolas' [excellent answer on this](http://stackoverflow.com/a/8114913/241631). – Praetorian Feb 05 '15 at 04:57
  • @Praetorian: This of course is the case that Nicol didn't address, and probably will change his answer. I left a comment there to bring it to his attention. – Ben Voigt Feb 05 '15 at 05:00
  • I guess the bug is now fixed, so there's no longer any reason to consume a smart-pointer by rvalue-reference? – Deduplicator Jan 02 '16 at 04:25
  • @Deduplicator: Pass-by-rvalue reference is still the best practice when taking over ownership. – Ben Voigt Jan 02 '16 at 07:02
  • @BenVoigt: why is it best practice to use pass-by-rvalue reference? I saw you mention https://stackoverflow.com/questions/8114276/how-do-i-pass-a-unique-ptr-argument-to-a-constructor-or-a-function#comment45019080_8114913 that the undefined order of argument evaluation was the reason. But I don't see how that affects functional behavior in the general case. So why would pass-by-rvalue-ref be better than pass-by-value? Especially given the fact that pass-by-value gives a strong guarantee that the passed argument will be moved from, whereas rvalue-ref just permits it. – Giel Jun 16 '16 at 11:47
1

The order is indeed undefined, but that doesn't matter because std::move doesn't actually move from the pointer, it only changes the value category.

The call to func(ptr->some_data) will take place before the pointer is moved, because the first is argument evaluation and the latter happens inside the base constructor, and argument evaluation always is ordered before a function call.

If it makes you feel better, you can write it as the 100% equivalent:

derive(unique_ptr<X> ptr): base{func(ptr->some_data), (unique_ptr<X>&&)ptr}{}

Edit: the actual move doesn't take place inside the called function, if the parameter is pass by-value. But who does such a thing with unique_ptrs?

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • @AMostMajestuousCapybara: It's not supposed to be pretty. Just clear. The elegant way is to keep using `std::move` – Ben Voigt Feb 05 '15 at 04:37
  • In fact, `move` takes place in first in my case. – cqdjyy01234 Feb 05 '15 at 04:38
  • @user1535111: The call to `std::move`, which is non-destructive? Or the call to the move-constructor of `unique_ptr`? The two are very different things. – Ben Voigt Feb 05 '15 at 04:39
  • @BenVoigt I mean the variable (e.g. `m_ptr`) that accepts `std::move(ptr)` is declared in the base of `base`, i.e. `m_ptr` is declared before `m_other_data`. – cqdjyy01234 Feb 05 '15 at 04:42
  • 1
    @user1535111 Can you please post these details in the question? And preferably make it compilable. – Praetorian Feb 05 '15 at 04:43