【问题标题】:C++ std::move a pointerC++ std::move 指针
【发布时间】:2017-05-28 06:59:49
【问题描述】:

我有一个 C++ 框架,我提供给我的用户,他们应该使用我用他们自己的实现编写的模板化包装器作为模板化类型。 包装器充当 RAII 类,它包含一个指向用户类实现的指针。 为了使用户的代码干净整洁(在我看来),我提供了一个转换运算符,它将我的包装器转换为它所持有的指针。这样(连同其他一些重载)用户可以像使用指针一样使用我的包装器(很像 shared_ptr)。

我遇到了一个极端情况,用户调用一个函数,该函数接受一个指向他的实现类的指针,在我的包装器上使用 std::move。这是它的外观示例:

#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;
}

[这当然只是案例的一个例子,包装器中还有更多方法,但我很确定在这种情况下不会在这里发挥作用]

用户和我一样,期望如果 std::move 在包装器上被调用,那么在调用 foo 之后包装器将是空的(或者至少像被移动一样被修改),但是在实际上,在foo 之前调用的唯一方法是强制转换运算符。

有没有办法使对foo 的调用可以区分对foo 的两次调用,即在使用和不使用std::move 的情况下调用?

编辑 感谢 Mooing Duck 的评论,我找到了一种方法,my_wrapper 知道需要哪个调用,但我真的不确定这是最好的方法,并且也会感谢 cmets:

使用以下两个来代替之前的强制转换运算符:

operator T*() & {
    return m_ptr;
}

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

现在operator T*() &amp;&amp; 在使用 std::move 调用时调用,operator T*() &amp; 在不使用它调用时调用。

【问题讨论】:

  • 你的意思是在调用点区分还是被调用者区分?
  • 被调用者是指my_impl 类型?
  • 用户期望不合理。如果您在某事上调用 std::move 并将其传递给不拥有移动对象的函数,那么期望该对象为空或更改是不合理的。例如:std::shared_ptr&lt;X&gt; foo; ... std::move(foo)-&gt;bar(); 这不应该改变 foo 因为bar 不占有。与bar(std::move(foo)); 相同。 std::move 只是给被调用函数permission 来移动对象,如果没有指定,它不会强制它移动它。
  • 就个人而言,我不喜欢它。 unique_ptr 没有做什么?从语义上讲,它应该表现得像一个指针,对吧?你为什么期望std::move(pointer) 做一些特别的事情?
  • 不相关,确认my_wrapper 遵循五规则,并且my_wrapper(T*) 构造函数是显式的。

标签: c++ c++11 templates operator-overloading move-semantics


【解决方案1】:

和我一样,用户期望如果 std::move 在包装器上被调用,那么在调用 foo 之后包装器将为空(或者至少像被移动一样被修改)

你的期望是错误的。只有在发生移动时才会修改它,即,如果某种资源的所有权被转移。但是调用foo 并没有做任何类似的事情,因为它只是访问包装器内保存的指针。调用 std::move 不会做任何事情,除了将其参数转换为一个右值,这不会改变它。某些通过引用接受右值的函数可能会修改它,所以std::move 启用了它,但它本身并没有这样做。如果您不将右值传递给此类函数,则不会进行任何修改。

如果你真的想让它为空,你可以添加一个重载来做到这一点:

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

但是……为什么?为什么要这样做?

如果你这样做,包装器不会留空:

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

并且不留空:

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

如果复制一个右值包装器不会让它为空,为什么要简单地访问它的成员让它为空呢?这没有任何意义。

即使你的包装器有一个移动构造函数和移动赋值运算符,所以上面的例子 dow 留空,但这并不意味着访问右值对象的成员应该修改物体。为什么将operator T* 转换为左值还是右值会产生逻辑上的差异?

(另外,你真的确定从包装的指针类型到 的隐式转换是一个好主意吗?提示:这不是一个好主意。通常更喜欢让你的转换显式,尤其是,如果您正在处理指向动态分配对象的指针。)

【讨论】:

  • 一年后,阅读您的答案是有道理的,我可以说第一次阅读这个答案对我来说是错误的,但我现在知道这只是因为我没有完全理解移动语义。谢谢:)
猜你喜欢
  • 1970-01-01
  • 2020-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-24
相关资源
最近更新 更多