STD的行为是危险的

Is the behaviour of std::get on rvalue-reference tuples dangerous?

本文关键字:危险 STD      更新时间:2023-10-16

以下代码:

#include <tuple>
int main ()
{
  auto f = [] () -> decltype (auto)
  {
    return std::get<0> (std::make_tuple (0));
  };
  return f ();
}

(默默地)以未定义的行为生成代码 - make_tuple返回的临时RVALUE通过std :: get&lt;>传播到返回类型。因此,它最终返回了一个临时范围的引用。在此处查看https://godbolt.org/g/x1uhsw。

现在,您可以说我对decltype(auto)的使用有错。但是在我的通用代码(其中可能是std::tuple<Foo &>的类型)中,我不想始终进行副本。我确实想从元组中提取确切的值或参考。

我的感觉是std::get的超载很危险:

template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&& 
get( tuple<Types...>&& t ) noexcept;

虽然将LVALUE引用传播到元组元素上可能是明智的,但我认为这不适合RVALUE参考。

我确定标准委员会非常仔细地考虑这一点,但是谁能向我解释为什么这被认为是最好的选择?

考虑以下示例:

void consume(foo&&);
template <typename Tuple>
void consume_tuple_first(Tuple&& t)
{
   consume(std::get<0>(std::forward<Tuple>(t)));
}
int main()
{
    consume_tuple_first(std::tuple{foo{}});
}

在这种情况下,我们知道std::tuple{foo{}}是暂时的,它将在consume_tuple_first(std::tuple{foo{}})表达式的整个过程中生存。

我们要避免任何不必要的副本并移动,但仍将foo{}的暂时性传播到consume

唯一的方法是让std::get使用临时std::tuple实例调用时返回 rvalue参考

wandbox上的实时示例


std::get<0>(std::forward<Tuple>(t))更改为std::get<0>(t)产生汇编错误(如预期)( wandbox )。


具有通过值返回的get替代方案会导致其他不必要的举动:

template <typename Tuple>
auto myget(Tuple&& t)
{
    return std::get<0>(std::forward<Tuple>(t));
}
template <typename Tuple>
void consume_tuple_first(Tuple&& t)
{
   consume(myget(std::forward<Tuple>(t)));
}

wandbox上的实时示例


但是谁能向我解释为什么这被认为是最好的选择?

因为它可以启用可选的通用代码,以无缝传播临时性 rvalue参考访问元组时。按值返回的替代方案可能会导致不必要的移动操作。

imho这是危险且非常可悲的,因为它破坏了"最重要的const"的目的:

通常,临时对象仅持续到其出现的完整表达式的末端。但是,C 故意指定将临时对象与堆栈上的参考结合到参考的参考,从而将临时寿命延长到参考本身的寿命,因此避免了否则会是常见的悬空引用误差。

鉴于上述引用,多年来,C 程序员已经了解到这是可以的:

X const& x = f( /* ... */ );

现在,考虑此代码:

struct X {
    void hello() const { puts("hello"); }
    ~X() { puts("~X"); }
};
auto make() {
    return std::variant<X>{};
}
int main() {
    auto const& x = std::get<X>(make()); // #1
    x.hello();
}

我相信任何人都应该原谅,以为第1行是可以的。但是,由于std::get返回将要破坏的对象的引用,因此x是一个悬空的参考。上面的代码输出:

~X
hello

表明,在调用hello()之前,x绑定的对象被破坏。Clang对问题发出了警告,但GCC和MSVC没有。如果(如在OP中)我们使用std::tuple而不是std::variant,则会发生同样的问题,但可悲的是,Clang不会发出警告。

std::optional和此value过载也发生了类似的问题:

constexpr T&& value() &&;

使用上面使用相同X的代码说明了问题:

auto make() {
    return std::optional{X{}};
}
int main() {
    auto const& x = make().value();
    x.hello();
}

输出为:

~X
~X
hello

与C 23的std::except及其方法value()error()相同,将自己提供更多相同的功能。

constexpr T&& value() &&;
constexpr E&& error() && noexcept;

我宁愿在维托里奥·罗密欧(Vittorio Romeo)的帖子中付出这一举动的代价。当然,我可以从第1行和#2中删除&来避免问题。我的观点是"最重要的const"规则。只是变得更加复杂,我们需要考虑表达是否涉及std::getstd::optional::valuestd::expected::valuestd::expected::error,...