首页 > 解决方案 > 如何在 return 语句中对“隐式”右值进行类型转换

问题描述

这是无效的吗?gcc 接受它,clang 和 msvc 不接受。

#include <memory>

class Holder {
    std::unique_ptr<int> data;
public:
    operator std::unique_ptr<int>() && { return std::move(data); }
};

std::unique_ptr<int> test()
{
    Holder val;
    return val;
}

假设我不想添加类似的东西std::unique_ptr<int> Holder::TakeData() { return std::move(data); },我能想到的唯一其他解决方法是在返回值中移动:

std::unique_ptr<int> test()
{
    Holder val;
    return std::move(val); // lets the conversion proceed
}

但是 gcc 9.3+ 有胆量告诉我这std::move是多余的(启用所有警告)。怎么回事?我gcc的意思是,不需要移动,当然,但没有其他东西接受代码。如果它不是 gcc,那么一些人以后不可避免地会犹豫不决。

  1. 关于它是否应该按原样编译的权威最后一句话是什么?
  2. 这样的代码应该怎么写?我应该把这个看似嘈杂的TakeData功能放入并使用它吗?更糟糕的是 - 我是否应该将TakeData功能限制为右值上下文,即必须这样做return std::move(val).TakeData()

添加operator std::unique_ptr<int>() & { return std::move(data); }不是一种选择,因为它显然会导致讨厌的错误 - 它可以在错误的上下文中调用。

标签: c++language-lawyer

解决方案


“隐式”右值转换是标准强制要求的。但是根据您使用的标准版本,哪个编译器“正确”会有所不同。

在 C++17 中

[class.copy.elision](强调我的)

3在以下复制初始化上下文中,可能会使用移动操作而不是复制操作:

  • 如果 return 语句中的表达式是一个(可能带括号的)id 表达式,它命名一个对象,该对象具有在最内层封闭函数或 lambda 表达式的主体或参数声明子句中声明的自动存储持续时间,或

  • ...

首先执行为副本选择构造函数的重载决策,就好像对象是由右值指定的一样。如果第一个重载决议失败或未执行,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是 cv 限定的),则再次执行重载决议,将对象视为左值。[注意:无论是否会发生复制省略,都必须执行此两阶段重载解析。它确定如果不执行省略则要调用的构造函数,并且即使调用被省略,所选构造函数也必须是可访问的。——尾注]

直到 C++17,GCC 都是错误的。由于我用粗体标记的句子( c'tor 中的右值引用不直接绑定到),隐式使用val作为右值应该无法初始化返回类型。但是到了 C++20,那句话就不再存在了。unique_ptrval

C++20

3隐式可移动实体是自动存储持续时间的变量,它可以是非易失性对象,也可以是对非易失性对象类型的右值引用。在以下复制初始化上下文中,可能会使用移动操作而不是复制操作:

  • 如果 return ([stmt.return]) 或 co_return ([stmt.return.coroutine]) 语句中的表达式是一个(可能带括号的)id-expression,它命名在正文或参数声明子句中声明的隐式可移动实体最里面的封闭函数或 lambda 表达式,或

  • [...]

首先执行为复制选择构造函数的重载决策或要调用的 return_value 重载,就好像表达式或操作数是右值一样。如果第一次重载决议失败或未执行,则再次执行重载决议,将表达式或操作数视为左值。[注意:无论是否会发生复制省略,都必须执行此两阶段重载解析。如果不执行省略,它确定要调用的构造函数或 return_value 重载,并且即使调用被省略,所选的构造函数或 return_value 重载也必须是可访问的。——尾注]

因此,代码的正确性取决于编译器的时间旅行属性。

至于应该如何编写这样的代码。如果您没有得到一致的结果,一个选项是使用函数的确切返回类型

std::unique_ptr<int> test()
{
    Holder val;
    std::unique_ptr<int> ret_val = std::move(val);
    return ret_val;
}

我从一开始就同意这可能不像简单地返回那么吸引人val,但至少它与 NRVO 配合得很好。所以我们不可能得到unique_ptr比我们最初想要的更多的副本。

如果这仍然太不吸引人,那么我发现你关于资源窃取成员函数的想法是我最喜欢的。但没有考虑到味道。


推荐阅读