c++ rvalue-reference元组上的std::get行为是否危险?

vktxenjb  于 2022-12-05  发布在  其他
关注(0)|答案(2)|浏览(174)

下面的代码:

#include <tuple>

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

  return f ();
}

(无声地)生成具有未定义行为的代码--make_tuple返回的临时右值通过std::get〈〉和decltype(auto)传播到返回类型上。因此它最终返回了一个超出作用域的临时值的引用。参见此处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;

虽然将左值引用传播到元组元素上可能是明智的,但我不认为这适用于右值引用。
我相信标准委员会会非常仔细地考虑过这个问题,但是谁能向我解释一下为什么这被认为是最好的选择?

rdlzhqv9

rdlzhqv91#

请考虑以下示例:

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示例调用时,std::get返回一个 * 右值引用 *。
live example on wandbox
std::get<0>(std::forward<Tuple>(t))变更为std::get<0>(t)会产生编译错误(如预期)(on 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)));
}

live example on wandbox
但谁能向我解释一下为什么这被认为是最好的选择呢?
因为它启用了可选的泛型代码,可以在访问元组时无缝地传播临时 * 右值引用 *。

8tntrjer

8tntrjer2#

IMHO this is dangerous and quite sad since it defeats the purpose of the "most important const " :
Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself, and thus avoids what would otherwise be a common dangling-reference error.
In light of the quote above, for many years C++ programmers have learned that this was OK:

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

Now, consider this code :

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();
}

I believe anyone should be forgiven for thinking that line #1 is OK. However, since std::get returns a reference to an object that is going to be destroyed, x is a dangling reference. The code above outputs:

~X
hello

which shows that the object that x binds to is destroyed before hello() is called. Clang gives a warning about the issue but gcc and msvc don't. The same issue happens if (as in the OP) we use std::tuple instead of std::variant but, sadly enough, clang doesn't issues a warning for this case.
A similar issue happens with std::optional and this value overload:

constexpr T&& value() &&;

This code , which uses the same X above, illustrates the issue:

auto make() {
    return std::optional{X{}};
}

int main() {
    auto const& x = make().value();
    x.hello();
}

The output is:

~X
~X
hello

Brace yourself for more of the same with C++23's std::except and its methods value() and error() :

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

I'd rather pay the price of the move explained in Vittorio Romeo's post . Sure, I can avoid the issue by removing & from lines #1 and #2. My point is that the rule for the "most important const " just became more complicated and we need to consider if the expression involves std::get , std::optional::value , std::expected::value , std::expected::error , ...

相关问题