C++ std::move a pointer

19,301

The user, as would I, expect that if std::move was called on the wrapper, then after the call to foo the wrapper will be empty (or at least modified as if it was moved)

Your expectation is wrong. It will only be modified if a move happens, i.e. if ownership of some kind of resource is transferred. But calling foo doesn't do anything like that, because it just gets access to the pointer held inside the wrapper. Calling std::move doesn't do anything except cast its argument to an rvalue, which doesn't alter it. Some function which accepts an rvalue by reference might modify it, so std::move enables that, but it doesn't do that itself. If you don't pass the rvalue to such a function then no modification takes place.

If you really want to make it empty you can add an overload to do that:

template<typename T>
void foo(my_wrapper<T>&& w) {
    foo(static_cast<my_interface*>(w));
    w = my_wrapper<T>{};  // leave it empty
}

But ... why? Why should it do that?

The wrapper isn't left empty if you do:

my_wrapper<my_impl> w(new my_impl);
my_wrapper<my_impl> w2 = std::move(w);

And isn't left empty by:

my_wrapper<my_impl> w(new my_impl);
my_wrapper<my_impl> w2;
w2 = std::move(w);

If copying an rvalue wrapper doesn't leave it empty, why should simply accessing its member leave it empty? That makes no sense.

Even if your wrapper has a move constructor and move assignment operator so that the examples above do leave w empty, that still doesn't mean that accessing the member of an rvalue object should modify the object. Why does it make any logical difference whether the operator T* conversion is done to an lvalue or an rvalue?

(Also, are you really sure that having implicit conversions both to and from the wrapped pointer type is a good idea? Hint: it's not a good idea. In general prefer to make your conversions explicit, especially if you're dealing with pointers to dynamically-allocated objects.)

Share:
19,301
ZivS
Author by

ZivS

_ _ _ | | | (_) | |_| | | | _ | | |_| |_|_|

Updated on June 04, 2022

Comments

  • ZivS
    ZivS almost 2 years

    I have a C++ framework which I provide to my users, who should use a templated wrapper I wrote with their own implementation as the templated type. The wrapper acts as an RAII class and it holds a pointer to an implementation of the user's class. To make the user's code clean and neat (in my opinion) I provide a cast operator which converts my wrapper to the pointer it holds. This way (along with some other overloads) the user can use my wrapper as if it is a pointer (much like a shared_ptr).

    I came across a corner case where a user calls a function, which takes a pointer to his implementation class, using std::move on my wrapper. Here's an example of what it looks like:

    #include <iostream>
    using namespace std;
    
    struct my_interface {
        virtual int bar() = 0;
    };
    
    template <typename T>
    struct my_base : public my_interface {
        int bar() { return 4; }
    };
    
    struct my_impl : public my_base<int> {};
    
    template <typename T>
    struct my_wrapper {
        my_wrapper(T* t) {
            m_ptr = t;
        }
    
        operator T*() {
            return m_ptr;
        }
    
    private:
        T* m_ptr;
    };
    
    void foo(my_interface* a) {
        std::cout << a->bar() << std::endl;
    }
    
    
    int main()
    {
        my_impl* impl = new my_impl();
        my_wrapper<my_impl> wrapper(impl);
        foo(std::move(wrapper));
        //foo(wrapper);
    
        return 0;
    }
    

    [This is ofcourse just an example of the case, and there are more methods in the wrapper, but I'm pretty sure that don't play a role here in this case]

    The user, as would I, expect that if std::move was called on the wrapper, then after the call to foo the wrapper will be empty (or at least modified as if it was moved), but in reality the only method being invoked before foo is the cast operator.

    Is there a way to make the call to foo distinguishable between the two calls to foo i.e when calling with and without std::move?

    EDIT Thanks to the Mooing Duck's comment I found a way that my_wrapper knows which call is required, but I'm really not sure this is the best method to go with and will appreciate comments on this as well:

    Instead of the previous cast operator use the following two:

    operator T*() & {
        return m_ptr;
    }
    
    operator T*() &&{
        //Do something
        return m_ptr;
    }
    

    now operator T*() && is called when calling with std::move and operator T*() & is called when calling without it.

    • Kerrek SB
      Kerrek SB over 7 years
      Do you mean distinguishable at the call site or by the callee?
    • ZivS
      ZivS over 7 years
      By the callee you mean the my_impl type?
    • David Schwartz
      David Schwartz over 7 years
      The user's expectation is unreasonable. If you call std::move on something and pass that to a function that does not take possession of the moved object, expecting the object to be empty or changed is unreasonable. For example: std::shared_ptr<X> foo; ... std::move(foo)->bar(); This should not change foo because bar does not take possession. Same if bar(std::move(foo));. The std::move just gives the called function permission to move the object, it doesn't force it to move it if it isn't specified to.
    • ZivS
      ZivS over 7 years
      @DavidSchwartz, so do you think that my_wrapper's API is "well defined" in this case?
    • David Schwartz
      David Schwartz over 7 years
      Personally, I don't like it. What does it do that unique_ptr doesn't do? Semantically, it's supposed to behave like a pointer, right? Why would you expect std::move(pointer) to do anything special?
    • ZivS
      ZivS over 7 years
      mainly it is itanium abi compliant
    • ZivS
      ZivS over 7 years
      "Why would you expect std::move(pointer) to do anything special?" - actually I was so caught up with the fact the the wrapper was being moved that I forgot this is how it should be treated, as a pointer
    • ZivS
      ZivS over 7 years
      still I wonder, if there is a way to have my_wrapper or my_interface detect that its pointer is sent to std::move?
    • Mooing Duck
      Mooing Duck over 7 years
      @ZivS: Sure. If operator T*() && is called, then someone passed it as an rvalue to a function expecting a T*.
    • Mooing Duck
      Mooing Duck over 7 years
      unrelated, affirm that my_wrapper obeys the rule of five, and that that my_wrapper(T*) constructor is explicit.
    • ZivS
      ZivS over 7 years
      @MooingDuck, it does not seem to compile with your suggestion
    • Kerrek SB
      Kerrek SB over 7 years
      @ZivS: No, I mean foo. Should foo know how it was called?
    • ZivS
      ZivS over 7 years
      @MooingDuck, please see my edit to the post
    • ZivS
      ZivS over 7 years
      @KerrekSB, no. foo is not even my function (as the API creator)
    • Kerrek SB
      Kerrek SB over 7 years
      Then what exactly do you want to achieve? Me, I would simply decide to not provide the implicit conversion from either an lvalue or an rvalue of wrapper. (But I'm not sure which one.)
  • ZivS
    ZivS over 6 years
    a year later, reading your answer makes sense and I can say that reading this answer the first time felt wrong to me, but I know now that it was only because I did not fully understood move semantics. Thanks :)