从 lambda (C++) 创建的 std::function 的奇怪 return 行为
Odd return behavior with std::function created from lambda (C++)
如果函数 return 是一个引用但 return 类型没有被明确调用为引用,我在使用从 lambdas 创建的 std::functions 时遇到了问题。 std::function 似乎创建得很好,没有警告,但在调用它时,当需要引用时,值被 returned,导致事情爆炸。这是一个非常人为的例子:
#include <iostream>
#include <vector>
#include <functional>
int main(){
std::vector<int> v;
v.push_back(123);
std::function<const std::vector<int>&(const std::vector<int>&)> callback =
[](const std::vector<int> &in){return in;};
std::cout << callback(v).at(0) << std::endl;
return 0;
}
这会打印出垃圾,但是如果将 lambda 修改为显式 return const 引用,它就可以正常工作。我可以理解编译器认为 lambda 是 return-by-value 没有提示(当我最初 运行 进入这个问题时,lambda 直接 returning 来自函数的结果returned 一个 const 引用,在这种情况下我认为 lambda 的 const 引用 return 是可以推导的,但显然不是。)令我惊讶的是编译器让 std::function 由 return 类型不匹配的 lambda 构造。这种行为是预期的吗?我是否遗漏了标准中允许发生这种不匹配的内容?我在 g++ (GCC) 4.8.2 上看到了这个,还没有用其他任何东西尝试过。
谢谢!
为什么坏了?
当推导出 lambda 的 return 类型时,将删除引用和 cv 限定。所以
的 return 类型[](const std::vector<int> &in){return in;};
只是 std::vector<int>
,而不是 std::vector<int> const&
。因此,如果我们去掉代码中的 lambda 和 std::function
部分,我们实际上有:
std::vector<int> lambda(std::vector<int> const& in)
{
return in;
}
std::vector<int> const& callback(std::vector<int> const& in)
{
return lambda(in);
}
lambda
return是临时的。它实际上只是复制了它的输入。此临时文件绑定了 callback
中的引用 return。但是绑定到 return
语句中引用的临时对象的生命周期没有延长,因此临时对象在 return 语句结束时被销毁。因此,此时:
callback(v).at(0)
-----------^
我们有一个对 v
的已销毁 副本 的悬空引用。
解决方法是显式指定lambda的return类型作为引用:
[](const std::vector<int> &in)-> const std::vector<int>& {return in;}
[](const std::vector<int> &in)-> decltype(auto) {return in;} // C++14
现在没有副本,没有临时文件,没有悬空引用,也没有未定义的行为。
谁对谁错?
至于这是否是预期的行为,答案实际上是肯定的。 std::function
的构造条件是 [func.wrap.func.con]:
f
is Callable (20.9.12.2) for argument typesArgTypes...
and return typeR
.
其中,[func.wrap.func]:
A callable object
f
of typeF
is Callable for argument typesArgTypes
and return typeR
if the expressionINVOKE (f, declval<ArgTypes>()..., R)
, considered as an unevaluated operand (Clause 5), is well formed (20.9.2).
其中,[func.require],强调我的:
Define
INVOKE(f, t1, t2, ..., tN, R)
asstatic_cast<void>(INVOKE (f, t1, t2, ..., tN))
ifR
is cvvoid
, otherwiseINVOKE(f, t1, t2, ..., tN)
implicitly converted toR
.
所以,如果我们有:
T func();
std::function<T const&()> wrapped(func);
实际上满足所有标准要求:INVOKE(func)
是合式的,而 return 是 T
,T
是 可隐式转换为 T const&
。所以这不是 gcc 或 clang 错误。这可能是一个标准缺陷,因为我不明白为什么你会想要允许这样的构造。这将 永远不会 有效,因此措辞可能会要求如果 R
是引用类型,则 F
也必须 return 是引用类型.
我自己搜索了一些关于 std::function
构造函数的信息。这部分似乎是对 std::function
和标准的 Callable
concept 交互的疏忽。 std::function<R(Args...)>::function<F>(F)
要求 F
为 Callable
为 R(Args...)
,这本身似乎是合理的。 Callable
for R(Args...)
需要 F
的 return 类型(当给定类型 Args...
的参数可以隐式转换为 R
,这也在本身似乎是合理的。现在当 R
是 const R_ &
时,这将允许 R_
到 const R_ &
的隐式转换,因为允许 const 引用绑定到右值。这不一定不安全。例如,考虑一个函数 f()
,它 return 是一个 int
,但被认为是可调用的 const int &()
。
const int &result = f();
if ( f == 5 )
{
// ...
}
这里没有问题,因为 C++ 的延长临时文件生命周期的规则。 但是,以下具有未定义的行为:
std::function<const int &()> fWrapped{f};
if ( fWrapped() == 5 )
{
// ...
}
这是因为生命周期延长在这里不适用。临时文件在 std::function
的 operator()
内部创建,并在比较之前销毁。
因此,std::function
的构造函数可能不应该单独依赖 Callable
,而是强制执行额外的限制,即将右值隐式转换为 const
左值以便绑定禁止引用。或者,可以将 Callable
更改为永远不允许这种转换,但代价是不允许某些安全使用(如果仅仅是因为生命周期延长)。
为了让事情变得更复杂,调用上面示例中的 fWrapped()
是完全安全的,只要您不访问悬空引用的目标。
如果您使用:
return std::ref(in);
在你的 lambda 中它会起作用。
这将使您的 lambda 的 return 类型为 std::reference_wrapper<std::vector<int>>
,它可以隐式转换为 std::vector<int>&
.