12

使用转发引用时,将相同的值转发给多个函数是不是一个坏主意?考虑以下代码:

template<typename Container>
constexpr auto
front(Container&& c)
-> typename Container::value_type
{ return std::forward<Container>(c).front(); }

template<typename Container>
constexpr auto
back(Container&& c)
-> typename Container::value_type
{ return std::forward<Container>(c).back(); }

template<typename Container>
constexpr auto
get_corner(Container&& c)
{
    return do_something(front(std::forward<Container(c)),
                        back(std::forward<Container>(c));
}

如果Container是左值引用,则该函数可以正常工作。但是,我担心将右值传递给它的情况,因为一旦发生移动,该值就会失效。我的疑问是:在这种情况下是否有正确的方法来转发容器,而不会丢失值类别?

4

4 回答 4

14

一般来说,同一个函数转发同一个参数两次是不合理的。除非它对转发参数的接收者将做什么有特定的了解。

请记住: 的行为std::forward可以等同于 的行为std::move,具体取决于用户传入的参数。xvalue 的行为将取决于接收函数如何处理它。如果接收器采用非 const 右值引用,则可能从该值移动。那会让你拿着一个移出的物体。如果它需要一个值,如果类型支持它,它肯定会从它移开。

因此,除非您对正在使用的操作的预期行为有特定的了解,否则多次转发参数是不安全的。

于 2016-09-05T00:20:22.697 回答
4

实际上没有右值引用版本std::begin- 我们只有(留出constexpr并返回值):

template <class C>
??? begin(C& );

template <class C>
??? begin(C const& );

对于左值容器,你得到iterator,而对于右值容器,你得到const_iterator(或任何容器特定的等价物最终是)。

您的代码中的一个真正问题是返回decltype(auto). 对于左值容器,这很好 - 您将返回对生命周期超过函数的对象的引用。但是对于右值容器,这将返回一个悬空引用。您需要返回左值容器的引用和右值容器的

最重要的是,forward将容器 -ing 到begin()/end()可能不是您想要做的。select()有条件地将结果包装为移动迭代器会更有效。像我的这个答案

template <typename Container,
          typename V = decltype(*std::begin(std::declval<Container&>())),
          typename R = std::conditional_t<
              std::is_lvalue_reference<Container>::value,
              V,
              std::remove_reference_t<V>
              >
          >
constexpr R operator()(Container&& c)
{
    auto it = select(std::begin(c), std::end(c));
    return *make_forward_iterator<Container>(it);
}

可能有一种不那么冗长的方式来表达所有这些。

于 2016-09-04T22:50:46.457 回答
3

您大概意识到您不希望将std::move一个对象传递给多个函数:

std::string s = "hello";
std::string hello1 = std::move(s);
std::string hello2 = std::move(s);  // hello2 != "hello"

的作用forward只是恢复参数在传递给函数时的任何右值状态。

forward我们可以通过对具有移动效果的函数两次 ing 一个参数来快速证明这是一种不好的做法:

#include <iostream>
#include <string>

struct S {
    std::string name_ = "defaulted";
    S() = default;
    S(const char* name) : name_(name) {}
    S(S&& rhs) { std::swap(name_, rhs.name_); name_ += " moved"; }
};

void fn(S s)
{
    std::cout << "fn(" << s.name_ << ")\n";
}

template<typename T>
void fwd_test(T&& t)
{
    fn(std::forward<T>(t));
    fn(std::forward<T>(t));
}

int main() {
    fwd_test(S("source"));
}

http://ideone.com/NRM8Ph

如果转发是安全的,我们应该看到fn(source moved)两次,但我们看到的是:

fn(source moved)
fn(defaulted moved)
于 2016-09-05T00:34:47.717 回答
2

一般来说,是的,这是有潜在危险的。

转发一个参数可以确保如果通用引用参数接收到的值是某种右值,那么在转发时它将继续是一个右值。如果该值最终被转发到一个函数(例如移动构造函数),该函数通过从该值移动来使用该值,则其内部状态可能无法在后续调用中使用。

如果您不转发参数,它(通常)将没有资格进行移动操作,因此您可以避免此类行为。

在您的情况下,front并且back(自由函数和成员函数)不会对容器执行移动,因此您给出的具体示例应该是安全的。但是,这也表明没有理由转发容器,因为不会给予右值与左值不同的处理——这是通过首先转发值来保持区别的唯一原因。

于 2016-09-05T00:23:51.180 回答