关于 C++ functor/lambda STL 算法中参数传递的困惑
Confusion about C++ functor/lambda argument-passing in STL algorithms
我喜欢 C++11 及其将 STL 算法与 lambda 相结合的能力;它使 STL 对每个人都更容易理解和有用。但是我不明白的一件事是在 STL 算法(如 std::accumulate
)中发生的关于对象复制或在 lambda(或你给它的任何仿函数)中引用的事情。
我的三个问题是:
- 关于在 lambdas/functors 中我应该关心按引用传递还是按值传递的任何地方,是否有任何指导方针?
- 如果您在通过引用获取其参数的算法中声明一个 lambda (
[](Type &a, Type &b){}
),这是否重要,它是否比常规变体更优化?或者它只是语法糖,编译器无论如何都会优化它,我可以简单地省略 & 符号吗?
- C++标准对此有规定吗?
至于问题 #2,Godbolt's GCC 页面中的一个快速实验(使用编译标志 -stc=c++11 -Os
)似乎暗示了后者,因为无论我使用什么,下面代码生成的程序集都是相同的[](T i1, T i2)
或 [](T &i1, T &i2)
;但是,我不知道这些结果是否可以推广到更复杂的 types/objects。
示例 #1:
#include<array>
#include<numeric>
template <typename T>
T vecSum(std::array<T, 4> &a){
return std::accumulate(a.begin(), a.end(), T(0),
[](T i1, T i2) {
return std::abs(i1) + std::abs(i2);
}
);
}
void results() {
std::array<int, 4> a = {1,-2, 3,-4};
std::array<int, 4> b = {1,-2,-3, 4};
volatile int c = vecSum(a) + vecSum(b);
}
示例 #2:
#include<string>
#include<array>
#include<numeric>
struct FatObject {
std::array<int, 1024*1024> garbage;
std::string string;
FatObject(const std::string &str) : string(str) {
std::fill(garbage.begin(),garbage.end(),0xCAFEDEAD);
}
std::string operator+(const FatObject &rhs) const {
return string + rhs.string;
}
};
template <typename T>
T vecSum(std::array<T, 4> &a){
return std::accumulate(a.begin(),a.end(),T(0),
[](T i1, T i2) {
return i1 + i2;
}
);
}
void results() {
std::array<FatObject, 4> a = {
FatObject("The "),
FatObject("quick "),
FatObject("brown "),
FatObject("fox")
};
std::array<FatObject, 4> b = {
FatObject("jumps "),
FatObject("over "),
FatObject("the "),
FatObject("dog ")
};
volatile std::string c = vecSum(a) + vecSum(b);
}
你的问题很宽泛,但这是我的简洁回答。
1) 通常,在 lambda 或仿函数中按值传递与按引用传递的准则与任何常规函数或方法的准则相同(lambda 是为您动态创建的仿函数,这是一个带有 operator()(T)
) 的对象。该选择主要针对您的情况,例如,如果 lambda/functor 需要对其参数进行只读访问,您通常会传递一个 const 引用。
2) 在接受可调用对象作为参数(和模板参数)的算法内部,编译器必须遵守语言规则。因此,参数将根据 lambda/functor 的签名在内部按值或引用传递。
请记住,复制省略可能会起作用,但这是一个单独的问题,与您在标准库算法中调用 lambda 的事实没有直接关系。
带int
的例子太简单了。我建议你用实物来试验。
3) C++ 标准为发生复制省略的条件提供了精确的 definitions,以及对特定标准库算法的 lambda/functor 参数签名的要求。
但是,一般来说,要知道特定算法的内部实现是否会以满足复制省略条件的方式调用 lambda/functor 并不容易。
请注意,对签名的要求具有一定程度的灵活性,例如在 std::accumulate
documentation 我们有
The signature of the function should be equivalent to the following:
Ret fun(const Type1 &a, const Type2 &b); The signature does not need
to have const &.
因此您可以根据需要选择按值或按引用传递。
lambdas/functions 中的规则与所有 C++ 中的规则相同。
如果函数的目的是为调用者修改对象,则应使用非常量引用。如果该函数只是使用该对象而不更改它,则该函数应使用 const&。如果它要 copy/move 将对象放入其内部存储,它应该按值传递。
如果您传递像 int
这样的小对象,那么按值传递或按引用传递都没有区别。
当你开始传递一个大对象时,它会对性能产生很大影响。
我喜欢 C++11 及其将 STL 算法与 lambda 相结合的能力;它使 STL 对每个人都更容易理解和有用。但是我不明白的一件事是在 STL 算法(如 std::accumulate
)中发生的关于对象复制或在 lambda(或你给它的任何仿函数)中引用的事情。
我的三个问题是:
- 关于在 lambdas/functors 中我应该关心按引用传递还是按值传递的任何地方,是否有任何指导方针?
- 如果您在通过引用获取其参数的算法中声明一个 lambda (
[](Type &a, Type &b){}
),这是否重要,它是否比常规变体更优化?或者它只是语法糖,编译器无论如何都会优化它,我可以简单地省略 & 符号吗? - C++标准对此有规定吗?
至于问题 #2,Godbolt's GCC 页面中的一个快速实验(使用编译标志 -stc=c++11 -Os
)似乎暗示了后者,因为无论我使用什么,下面代码生成的程序集都是相同的[](T i1, T i2)
或 [](T &i1, T &i2)
;但是,我不知道这些结果是否可以推广到更复杂的 types/objects。
示例 #1:
#include<array>
#include<numeric>
template <typename T>
T vecSum(std::array<T, 4> &a){
return std::accumulate(a.begin(), a.end(), T(0),
[](T i1, T i2) {
return std::abs(i1) + std::abs(i2);
}
);
}
void results() {
std::array<int, 4> a = {1,-2, 3,-4};
std::array<int, 4> b = {1,-2,-3, 4};
volatile int c = vecSum(a) + vecSum(b);
}
示例 #2:
#include<string>
#include<array>
#include<numeric>
struct FatObject {
std::array<int, 1024*1024> garbage;
std::string string;
FatObject(const std::string &str) : string(str) {
std::fill(garbage.begin(),garbage.end(),0xCAFEDEAD);
}
std::string operator+(const FatObject &rhs) const {
return string + rhs.string;
}
};
template <typename T>
T vecSum(std::array<T, 4> &a){
return std::accumulate(a.begin(),a.end(),T(0),
[](T i1, T i2) {
return i1 + i2;
}
);
}
void results() {
std::array<FatObject, 4> a = {
FatObject("The "),
FatObject("quick "),
FatObject("brown "),
FatObject("fox")
};
std::array<FatObject, 4> b = {
FatObject("jumps "),
FatObject("over "),
FatObject("the "),
FatObject("dog ")
};
volatile std::string c = vecSum(a) + vecSum(b);
}
你的问题很宽泛,但这是我的简洁回答。
1) 通常,在 lambda 或仿函数中按值传递与按引用传递的准则与任何常规函数或方法的准则相同(lambda 是为您动态创建的仿函数,这是一个带有 operator()(T)
) 的对象。该选择主要针对您的情况,例如,如果 lambda/functor 需要对其参数进行只读访问,您通常会传递一个 const 引用。
2) 在接受可调用对象作为参数(和模板参数)的算法内部,编译器必须遵守语言规则。因此,参数将根据 lambda/functor 的签名在内部按值或引用传递。
请记住,复制省略可能会起作用,但这是一个单独的问题,与您在标准库算法中调用 lambda 的事实没有直接关系。
带int
的例子太简单了。我建议你用实物来试验。
3) C++ 标准为发生复制省略的条件提供了精确的 definitions,以及对特定标准库算法的 lambda/functor 参数签名的要求。
但是,一般来说,要知道特定算法的内部实现是否会以满足复制省略条件的方式调用 lambda/functor 并不容易。
请注意,对签名的要求具有一定程度的灵活性,例如在 std::accumulate
documentation 我们有
The signature of the function should be equivalent to the following: Ret fun(const Type1 &a, const Type2 &b); The signature does not need to have const &.
因此您可以根据需要选择按值或按引用传递。
lambdas/functions 中的规则与所有 C++ 中的规则相同。
如果函数的目的是为调用者修改对象,则应使用非常量引用。如果该函数只是使用该对象而不更改它,则该函数应使用 const&。如果它要 copy/move 将对象放入其内部存储,它应该按值传递。
如果您传递像 int
这样的小对象,那么按值传递或按引用传递都没有区别。
当你开始传递一个大对象时,它会对性能产生很大影响。