将相同的值转发给两个或多个函数
Forwarding the same value to two or more functions
使用转发引用时,转发
对多个功能具有相同的价值?考虑以下代码:
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
是一个左值引用,函数就可以正常工作。但是,我担心将右值传递给它的情况,因为一旦发生移动,该值就会失效。我的疑问是:在这种情况下是否有正确的方法来转发容器,而不会丢失值类别?
实际上没有 std::begin
的右值引用版本 - 我们只有(抛开 constexpr
和 return 值):
template <class C>
??? begin(C& );
template <class C>
??? begin(C const& );
对于左值容器,你得到 iterator
,对于右值容器,你得到 const_iterator
(或者任何容器特定的等价物最终成为)。
您的代码中的一个真正问题是 returning decltype(auto)
。对于左值容器,这很好 - 您将 return 引用一个生命周期超过函数的对象。但是对于右值容器,那是 returning 悬空引用。您需要 return reference 用于左值容器,value 用于右值容器。
最重要的是,forward
-ing 容器到 begin()
/end()
可能不是您想做的。将 select()
的结果有条件地包装为移动迭代器会更有效。类似于 this answer of mine:
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);
}
可能有一种不太冗长的方式来表达所有这些。
一般情况下,同一个函数转发同一个参数两次是不合理的。除非它特别了解转发参数的接收者将做什么。
请记住:std::forward
的行为可以等同于 std::move
的行为,具体取决于用户传入的参数。xvalue 的行为将取决于接收方式函数处理它。如果接收者采用非 const rvalue 引用,它可能会 move 从该值(如果可能)。那会让你持有一个移动的对象。如果它接受一个值,如果类型支持它,它肯定从它移动。
因此,除非您对正在使用的操作的预期行为有具体知识,否则多次转发一个参数是不安全的。
总的来说,是的,这有潜在的危险。
转发参数可确保如果通用引用参数接收到的值是某种右值,则在转发时它将继续是右值。如果该值最终被转发给一个函数(例如移动构造函数),该函数 消耗 通过从中移动该值,其内部状态可能无法在后续调用中使用.
如果您不转发该参数,它(通常)将不符合移动操作的条件,因此您不会受到此类行为的影响。
在您的情况下,front
和 back
(自由函数和成员函数)不会对容器执行移动,因此您给出的具体示例应该是安全的。然而,这也表明没有理由转发容器,因为不会给予右值与左值不同的处理——这是通过首先转发值来保持区别的唯一原因。
您可能意识到您不希望 std::move
将一个对象传递给多个函数:
std::string s = "hello";
std::string hello1 = std::move(s);
std::string hello2 = std::move(s); // hello2 != "hello"
forward
的作用只是恢复参数在传递给函数时的任何右值状态。
我们可以通过 forward
将一个参数两次传递给具有移动效果的函数来快速证明这是一种不好的做法:
#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"));
}
如果转发是安全的,我们应该会看到 fn(source moved)
两次,但我们会看到:
fn(source moved)
fn(defaulted moved)
使用转发引用时,转发 对多个功能具有相同的价值?考虑以下代码:
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
是一个左值引用,函数就可以正常工作。但是,我担心将右值传递给它的情况,因为一旦发生移动,该值就会失效。我的疑问是:在这种情况下是否有正确的方法来转发容器,而不会丢失值类别?
实际上没有 std::begin
的右值引用版本 - 我们只有(抛开 constexpr
和 return 值):
template <class C>
??? begin(C& );
template <class C>
??? begin(C const& );
对于左值容器,你得到 iterator
,对于右值容器,你得到 const_iterator
(或者任何容器特定的等价物最终成为)。
您的代码中的一个真正问题是 returning decltype(auto)
。对于左值容器,这很好 - 您将 return 引用一个生命周期超过函数的对象。但是对于右值容器,那是 returning 悬空引用。您需要 return reference 用于左值容器,value 用于右值容器。
最重要的是,forward
-ing 容器到 begin()
/end()
可能不是您想做的。将 select()
的结果有条件地包装为移动迭代器会更有效。类似于 this answer of mine:
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);
}
可能有一种不太冗长的方式来表达所有这些。
一般情况下,同一个函数转发同一个参数两次是不合理的。除非它特别了解转发参数的接收者将做什么。
请记住:std::forward
的行为可以等同于 std::move
的行为,具体取决于用户传入的参数。xvalue 的行为将取决于接收方式函数处理它。如果接收者采用非 const rvalue 引用,它可能会 move 从该值(如果可能)。那会让你持有一个移动的对象。如果它接受一个值,如果类型支持它,它肯定从它移动。
因此,除非您对正在使用的操作的预期行为有具体知识,否则多次转发一个参数是不安全的。
总的来说,是的,这有潜在的危险。
转发参数可确保如果通用引用参数接收到的值是某种右值,则在转发时它将继续是右值。如果该值最终被转发给一个函数(例如移动构造函数),该函数 消耗 通过从中移动该值,其内部状态可能无法在后续调用中使用.
如果您不转发该参数,它(通常)将不符合移动操作的条件,因此您不会受到此类行为的影响。
在您的情况下,front
和 back
(自由函数和成员函数)不会对容器执行移动,因此您给出的具体示例应该是安全的。然而,这也表明没有理由转发容器,因为不会给予右值与左值不同的处理——这是通过首先转发值来保持区别的唯一原因。
您可能意识到您不希望 std::move
将一个对象传递给多个函数:
std::string s = "hello";
std::string hello1 = std::move(s);
std::string hello2 = std::move(s); // hello2 != "hello"
forward
的作用只是恢复参数在传递给函数时的任何右值状态。
我们可以通过 forward
将一个参数两次传递给具有移动效果的函数来快速证明这是一种不好的做法:
#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"));
}
如果转发是安全的,我们应该会看到 fn(source moved)
两次,但我们会看到:
fn(source moved)
fn(defaulted moved)