在函数调用中访问和移动unique_ptr

4
我有一个类似于以下内容的段落。
struct derive : base{
    derive(unique_ptr ptr): base{func(ptr->some_data), std::move(ptr)}{}
};

理论上,它应该能够工作。但是由于编译器(vs2015)并没有严格遵循标准,因此func(ptr->some_data), std::move(ptr)的顺序是未定义的,即ptr可能在被访问之前就被移动了。
因此,我的问题是如何使这段代码按预期工作?
完整的代码如下:
#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) } {}
};

3
你有问题吗? - Ben Voigt
@Praetorian:不必担心base::base()的实现,除非编译器出现非常严重的错误(比Visual C++通常的不兼容性更糟糕),否则完全安全。 - Ben Voigt
@DrewDormann,每天都会有这样的时刻。 - WhozCraig
1
@BenVoigt 现在我有点困惑了(如果提供一个SSCCE,所有这些都可以避免!)。 我考虑的情况是,所讨论的base构造函数是base(result_of_func_type, unique_ptr)而不是base(result_of_func_type, unique_ptr&&)。 如果VS在评估顺序上不符合标准,则前者不安全,但后者是安全的。 - Praetorian
@Praetorian:聚合初始化可能仍然表现不同(但关于声明顺序规则的新评论排除了这种可能性)。 - Ben Voigt
显示剩余7条评论
2个回答

7
你可以使用构造函数链来修复它:
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)) {}
};

原因:如我在另一个答案中所解释的那样,对于 func 的调用肯定会在委托构造函数调用之前发生,而实际上移动 unique_ptr(而不仅仅是改变其值类别)肯定会在内部进行。
当然,这依赖于 Visual C++ 可能或可能没有正确实现的 C++11 的另一个特性。令人高兴的是,自从 VS2013 以来,委托构造函数被列为支持
更好的做法是只接受引用类型的 std::unique_ptr,如果您计划从它们那里窃取,请使用右值引用。 (如果您不会窃取内容,您为什么关心调用者有什么类型的智能指针? 只需接受原始的 T*。)
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) } {}
};

如果一开始没有这个问题,调用方的要求是完全不变的(需要提供rvalue,因为unique_ptr无法复制)。


制定这样一个通用规则的原因如下:按值传递可以复制或移动参数,以在调用点上选择最优操作。但是,std::unique_ptr是不可复制的,因此实际参数无论如何都必须是一个rvalue。


这是一个解决方案。如果没有更好的解决方案,我会接受它。 - cqdjyy01234
@user1535111:如果你打算从中窃取,那么是的。否则请使用const unique_ptr&。这就是我为什么说“总是”的原因。 - Ben Voigt
1
我已经为前半部分给了你一个+1,但是我不同意后半部分。我的观点是,如果您打算拥有所有权,则应按值获取unique_ptr,通过rvalue ref获取会让人感到模糊,您是否实际上从中移动了它。请参见NicolBolas的这个优秀答案 - Praetorian
我猜测这个 bug 现在已经被修复了,所以不再需要通过右值引用来消耗智能指针了吧? - Deduplicator
@Deduplicator:在接管所有权时,传递右值引用仍然是最佳实践。 - Ben Voigt
显示剩余3条评论

1
订单确实未定义,但这并不重要,因为 std::move 实际上并不移动指针,它只改变了值类别。 func(ptr->some_data) 的调用将在指针被移动之前发生,因为第一个是参数评估,而后者发生在基本构造函数内部,并且参数评估总是在函数调用之前排序。 如果这让你感觉更好,你可以将其写成100%等价的形式:
derive(unique_ptr<X> ptr): base{func(ptr->some_data), (unique_ptr<X>&&)ptr}{}

编辑:如果参数是按值传递的,则实际移动不会发生在被调用的函数内部。但是谁会这样使用unique_ptr呢?

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Ben Voigt
实际上,在我的情况下,move首先发生。 - cqdjyy01234
@user1535111:调用std::move,它是无损的吗?还是调用了unique_ptr的移动构造函数?这两件事情非常不同。 - Ben Voigt
@BenVoigt 我的意思是变量(例如 m_ptr)接受 std::move(ptr),它在 base 的基类中声明,即 m_ptrm_other_data 之前声明。 - cqdjyy01234
1
@user1535111,你能否把这些细节发布在问题中?最好能够编译通过。 - Praetorian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接