为什么 operator* of rvalue unique_ptr return 是左值?
Why does operator* of rvalue unique_ptr return an lvalue?
使用来自“死”unique_ptr 的 operator* 的 return 值是错误的。
以下代码可以编译,但当然会导致未定义的行为:
auto& ref = *std::make_unique<int>(7);
std::cout << ref << std::endl;
为什么标准没有为 std::unique_ptr 的右值创建 return 类型的运算符 * 内部值的 rvalue,而不是一个 lvalue,像这样:
// could have been done inside unique_ptr
T& operator*() & { return *ptr; }
T&& operator*() && { return std::move(*ptr); }
在这种情况下可以正常工作:
std::cout << *std::make_unique<int>(7) << std::endl;
但是开头的代码无法编译(无法将右值绑定到左值)。
旁注:当然有人可能仍然会写出像下面这样的糟糕代码,但恕我直言,它更冗长地说“我是 UB”,因此与此讨论不太相关:
auto&& ref = *std::make_unique<int>(7);
std::cout << ref << std::endl;
operator* 将 std::unique_ptr 的右值转换为 return 的左值引用有什么好的理由吗?
你的代码,就涉及的值类别和基本思想而言,相当于:
auto &ref = *(new int(7));
new int(7)
产生一个指针对象,它是一个纯右值表达式。取消引用该纯右值会导致左值表达式。
无论指针对象是右值还是左值,对指针应用*
都会得到左值。这不应该仅仅因为指针是 "smart".
而改变
std::cout << *std::make_unique<int>(7) << std::endl;
已经工作,因为临时在完整表达式的末尾消失。
T& operator*() & { return *ptr; }
T&& operator*() && { return std::move(*ptr); }
不会避免悬挂引用,(以您的示例为例)
auto&& ref = *std::make_unique<int>(7); // or const auto&
std::cout << ref << std::endl;
但实际上,会避免将临时值绑定到非常量左值引用。
另一个更安全的选择是:
T& operator*() & { return *ptr; }
T operator*() && { return std::move(*ptr); }
允许延长生命周期,但这会执行一般情况下不一定需要的额外移动构造函数。
问得好!
在没有深入研究相关论文和设计讨论的情况下,我认为有几点可能是这个设计决定的原因:
正如@Nicol Bolas 提到的,这是内置(原始)指针的行为方式,因此 "do as int
does" 在这里应用为 "do as int*
does"。
这类似于 unique_ptr
(和其他库类型)不传播 const
ness(这反过来也是我们添加 propagate_const
的原因)的事实。
下面的代码片段呢?它不会根据您建议的更改进行编译,但它是不应被阻止的有效代码。
class Base { virtual ~Base() = default; };
class Derived : public Base {};
void f(Base&) {}
int main()
{
f(*std::make_unique<Derived>());
}
(godbolt - 如果我们的 operator*
重载被注释掉,它会编译)
对于您的旁注:我不确定 auto&&
说 "I'm UB" 是否更大声。相反,有些人会争辩说 auto&&
应该是我们在许多情况下的默认值(例如基于范围的 for 循环;甚至建议为 "terse-notation range-based for loop" 自动插入(这不被接受,但是仍然...))。让我们记住 rvalue-ref 具有与 const &
相似的效果,延长临时文件的生命周期(在已知限制内),因此它不一定看起来像一般的 UB。
使用来自“死”unique_ptr 的 operator* 的 return 值是错误的。
以下代码可以编译,但当然会导致未定义的行为:
auto& ref = *std::make_unique<int>(7);
std::cout << ref << std::endl;
为什么标准没有为 std::unique_ptr 的右值创建 return 类型的运算符 * 内部值的 rvalue,而不是一个 lvalue,像这样:
// could have been done inside unique_ptr
T& operator*() & { return *ptr; }
T&& operator*() && { return std::move(*ptr); }
在这种情况下可以正常工作:
std::cout << *std::make_unique<int>(7) << std::endl;
但是开头的代码无法编译(无法将右值绑定到左值)。
旁注:当然有人可能仍然会写出像下面这样的糟糕代码,但恕我直言,它更冗长地说“我是 UB”,因此与此讨论不太相关:
auto&& ref = *std::make_unique<int>(7);
std::cout << ref << std::endl;
operator* 将 std::unique_ptr 的右值转换为 return 的左值引用有什么好的理由吗?
你的代码,就涉及的值类别和基本思想而言,相当于:
auto &ref = *(new int(7));
new int(7)
产生一个指针对象,它是一个纯右值表达式。取消引用该纯右值会导致左值表达式。
无论指针对象是右值还是左值,对指针应用*
都会得到左值。这不应该仅仅因为指针是 "smart".
std::cout << *std::make_unique<int>(7) << std::endl;
已经工作,因为临时在完整表达式的末尾消失。
T& operator*() & { return *ptr; }
T&& operator*() && { return std::move(*ptr); }
不会避免悬挂引用,(以您的示例为例)
auto&& ref = *std::make_unique<int>(7); // or const auto&
std::cout << ref << std::endl;
但实际上,会避免将临时值绑定到非常量左值引用。
另一个更安全的选择是:
T& operator*() & { return *ptr; }
T operator*() && { return std::move(*ptr); }
允许延长生命周期,但这会执行一般情况下不一定需要的额外移动构造函数。
问得好!
在没有深入研究相关论文和设计讨论的情况下,我认为有几点可能是这个设计决定的原因:
正如@Nicol Bolas 提到的,这是内置(原始)指针的行为方式,因此 "do as
int
does" 在这里应用为 "do asint*
does"。这类似于
unique_ptr
(和其他库类型)不传播const
ness(这反过来也是我们添加propagate_const
的原因)的事实。下面的代码片段呢?它不会根据您建议的更改进行编译,但它是不应被阻止的有效代码。
class Base { virtual ~Base() = default; };
class Derived : public Base {};
void f(Base&) {}
int main()
{
f(*std::make_unique<Derived>());
}
(godbolt - 如果我们的 operator*
重载被注释掉,它会编译)
对于您的旁注:我不确定 auto&&
说 "I'm UB" 是否更大声。相反,有些人会争辩说 auto&&
应该是我们在许多情况下的默认值(例如基于范围的 for 循环;甚至建议为 "terse-notation range-based for loop" 自动插入(这不被接受,但是仍然...))。让我们记住 rvalue-ref 具有与 const &
相似的效果,延长临时文件的生命周期(在已知限制内),因此它不一定看起来像一般的 UB。