3

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.

ZivS
  • 2,094
  • 2
  • 27
  • 48
  • Do you mean distinguishable at the call site or by the callee? – Kerrek SB Jan 13 '17 at 00:37
  • By the callee you mean the `my_impl` type? – ZivS Jan 13 '17 at 00:39
  • 3
    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 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. – David Schwartz Jan 13 '17 at 00:40
  • @DavidSchwartz, so do you think that `my_wrapper`'s API is "well defined" in this case? – ZivS Jan 13 '17 at 00:43
  • 4
    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? – David Schwartz Jan 13 '17 at 00:44
  • mainly it is itanium abi compliant – ZivS Jan 13 '17 at 00:45
  • "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 Jan 13 '17 at 00:48
  • still I wonder, if there is a way to have my_wrapper or my_interface detect that its pointer is sent to std::move? – ZivS Jan 13 '17 at 00:51
  • @ZivS: Sure. If `operator T*() &&` is called, then someone passed it as an rvalue to a function expecting a `T*`. – Mooing Duck Jan 13 '17 at 00:54
  • 1
    unrelated, affirm that `my_wrapper` obeys the rule of five, and that that `my_wrapper(T*)` constructor is explicit. – Mooing Duck Jan 13 '17 at 00:54
  • @MooingDuck, it does not seem to compile with your suggestion – ZivS Jan 13 '17 at 01:01
  • @ZivS: No, I mean `foo`. Should `foo` know how it was called? – Kerrek SB Jan 13 '17 at 01:08
  • @MooingDuck, please see my edit to the post – ZivS Jan 13 '17 at 01:13
  • @KerrekSB, no. foo is not even my function (as the API creator) – ZivS Jan 13 '17 at 01:14
  • 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.) – Kerrek SB Jan 13 '17 at 01:45

1 Answers1

7

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.)

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • 4
    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 :) – ZivS Jan 06 '18 at 19:47