从c++ 17起,有一个特性可以计算在一个参数包的所有参数上使用二进制运算符的结果(带有一个可选的初值)。
例如,下面的函数返回所有传递参数的和:
#include <iostream>
#include <string>
template<typename... T>
auto foldSum1(T... args)
{
return (... + args); // ((arg1 + arg2) + arg3) ...
}
int main(void)
{
auto i1 = foldSum1(47, 11, 81, -1);
auto s1 = foldSum1(std::string("Hello "), "World ", "!");
return 0;
}
结果如下:
注意,返回表达式带有的小括号是折叠表达式的一部分,不能省略。
对于调用 foldSum1(47, 11, 81, -1),实例化的模板执行:return 47 + 11 + 81 + -1;
对于调用foldSum1(std::string("Hello "), "World ", "!"),实例化的模板执行: return Hello + World + !;
还请注意,折叠表达式参数的顺序可能不同,而且很重要(而且可能看起来有点违反直觉):
(... + args)
的结果是
((arg1 + arg2) + arg3) ...
也可以如:
(args + ...)
其结果是
(arg1 + (arg2 + arg3)) ...
1. 折叠表达式的动机
折叠表达式避免了递归实例化模板来对参数包的所有参数执行操作的需要。在C++17以前的实现如下:
template<typename T>
auto foldSumRec (T arg)
{
return arg;
}
template<typename T1, typename... Ts>
auto foldSumRec (T1 arg1, Ts... otherArgs)
{
return arg1 + foldSumRec(otherArgs...);
}
这样的实现不仅编写起来很麻烦。对于程序员和编译器来说,折叠表达式做的工作量都会显著减少:
template<typename... T>
auto foldSum (T... args)
{
return (... + args); // arg1 + arg2 + arg3 ...
}
2. 折叠表达式的使用
给定一个参数args和一个操作符op, c++ 17允许我们编写:
- 既可以是一元左折叠表达式:
( ... op args ) //扩展为:(((arg1 op arg2) op arg3) op…
- 也可以是一元右折叠表达式:
( args op ... )//扩展为:arg1 op (arg2 op . .(argN-1 op argN))
这里的括号是必需的。但是,圆括号和省略号(…)不必用空格分隔。
左折叠表达式和右折叠表达式之间的差异比预期的更为重要。例如,即使使用运算符+,可能也会有不同的效果。使用左侧折叠表达式时:
template<typename... T>
auto foldSumL(T... args)
{
return (... + args); // ((arg1 + arg2) + arg3) ...
}
然后调用:
foldSumL(1, 2, 3);
计算结果为:((1 + 2) + 3)
这也意味着下面的例子编译:
std::cout << foldSumL(std::string("hello"), "world", "!") << '\n'; // OK
请记住,std::string的运算符+,提供至少一个操作数是std::string。对于左折叠表达式来说,首先计算的是:
std::string("hello") + "world"
这个运算结果返回std:string,因此再次operator + 字符串常量值"!"也是合法的。
然而,如果调用如下:
std::cout << foldSumL("hello", "world", std::string("!")) << '\n'; // ERROR
编译错误,因为计算调用方式为:
("hello" + "world") + std::string("!")
两个字符串常量值使用operator +是不允许的。
如果我们实现为右折叠表达式:
template<typename... T>
auto foldSumR(T... args)
{
return (args + ...); // (arg1 + (arg2 + arg3)) ...
}
然后调用foldSumR(1, 2, 3),计算调用方式为:(1 + (2 + 3))。
这意味着下面的例子不再编译通过:
std::cout << foldSumR(std::string("hello"), "world", "!") << '\n'; // ERROR
现在如下调用是可以编译的:
std::cout << foldSumR("hello", "world", std::string("!")) << '\n'; // OK
几乎所有的情况下,都是从左到右求值,通常,应该首选带有参数包的左折叠语法(除非这不起作用):
(…+ args);//折叠表达式的首选语法
2.1 空参数包的处理
如果折叠表达式与空参数包一起使用,则应用以下规则:
- 如果使用运算符&&,则值为true。
- 如果使用操作符||,则值为false。
- 如果使用逗号运算符,则值为void()。
对于所有其他情况(通常),都可以添加一个初值:给定一个参数包args、一个初始值和一个操作符op, c++ 17也允许我们编写任何一种情况:
- 对于两元左折叠表达式:( value op ... op args ),扩展为:(((value op arg1) op arg2) op arg3) op…
- 对于两元右折叠表达式:( args op ... op value ),扩展为:arg1 op (arg2 op . . . (argN op value)))
op运算符必须在省略号两边是相同的。
#include <iostream>
#include <string>
template<typename First, typename... Rest>
First sum(First&& first, Rest&& ... rest)
{
return (first + ... + rest);
}
int main()
{
std::cout << sum(std::string("hello"), " world", " !");
return 0;
}
结果如下:
例如,下面的定义允许在添加值时传递空参数包:
template<typename... T>
auto foldSum (T... s)
{
return (0 + ... + s); // even works if sizeof...(s)==0
}
从概念上讲,我们添加0作为第一个操作数还是最后一个操作数并不重要:
template<typename... T>
auto foldSum (T... s)
{
return (s + ... + 0); // even works if sizeof...(s)==0
}
但是对于一元折叠表达式求值顺序很重要。对于二元折叠表达式也应该优选左折叠表达式:
(val + ... + args); // preferred syntax for binary fold expressions
此外,第一个操作数可能是特殊的,例如在本例中:
template<typename... T>
void print (const T&... args)
{
(std::cout << ... << args) << '\n';
}
在这里,传递的第一个参数调用print()返回的ostream执行其他参数的输出。其他实现可能无法编译,甚至无法执行一些意想不到的操作。例如:
std::cout << (args << ... << '\n');
像print(1)这样的调用将编译成功,但是打印值1左移了“\0”的值(通常为10),因此结果输出为1024。
#include <iostream>
template<typename ... T>
void print(const T& ... args)
{
(std::cout << ... << args) << std::endl;
}
template<typename ... T>
void print2(const T& ... args)
{
std::cout << (args << ... << '\n') << std::endl;
}
int main()
{
print(1);
print2(1);
print("hello", 42, "world");
return 0;
}
结果如下:
注意,在这个print()示例中,没有空格分隔参数包的所有元素。打印(“hello”,42,“world”)这样的调用将打印:hello42world
要按空格分隔传递的元素,需要一个辅助函数来确保除第一个参数外的任何参数的输出都由一个前导空格扩展。例如,这可以用辅助函数template spaceBefore()完成:
#include <iostream>
template<typename T>
const T& spaceBefore(const T& arg)
{
std::cout << ' ';
return arg;
}
template <typename First, typename... Args>
void print(const First& firstarg, const Args& ... args)
{
std::cout << firstarg;
(std::cout << ... << spaceBefore(args)) << '\n';
}
int main()
{
print("hello", 42, "world");
return 0;
}
结果如下:
在这里,
(std::cout << ... << spaceBefore(args))
折叠表达式被扩展为:
std::cout << spaceBefore(arg1) << spaceBefore(arg2) << ...
因此,对于参数包args中的每个元素,它调用一个辅助函数,在返回传递的参数之前打印一个空格字符,并将其写入std::cout。为了确保这不适用于第一个参数,我们添加了一个不使用spaceBefore()的附加参数给第一个参数。
注意,参数包输出的计算要求在实际元素调用spaceBefore()之前完成前面参数的所有输出。运算符<<的求值输出顺序自从C++17开始就可以保证了。
我们也可以使用lambda来定义print()中的sapcebefore ():
template<typename First, typename... Args>
void print (const First& firstarg, const Args&... args)
{
std::cout << firstarg;
auto spaceBefore = [](const auto& arg) {
std::cout << ' ';
return arg;
};
(std::cout << ... << spaceBefore(args)) << '\n';
}
但是,请注意lambdas默认情况下按值返回对象,这意味着这将创建传递的参数的不必要副本。避免这种情况的方法是显式声明lambda表达式返回类型为const auto&或者是decltype(auto):
template<typename First, typename... Args>
void print (const First& firstarg, const Args&... args)
{
std::cout << firstarg;
auto spaceBefore = [](const auto& arg) -> const auto&
//auto spaceBefore = [](const auto& arg) -> decltype(auto)
{
std::cout << ' ';
return arg;
};
(std::cout << ... << spaceBefore(args)) << '\n';
}
如果你不能把所有这些结合到一个语句中,c++就不是c++了:
template<typename First, typename... Args>
void print (const First& firstarg, const Args&... args)
{
std::cout << firstarg;
(std::cout << ... << [](const auto& arg) -> decltype(auto)
{
std::cout << ' ';
return arg;
}(args)) << '\n';
}
不过,实现print()的一种更简单的方法是使用一个lambda,它同时打印空格和参数,并将其传递给一元折叠:
template<typename First, typename... Args>
void print(First first, const Args&... args)
{
std::cout << first;
auto outWithSpace = [](const auto& arg)
{
std::cout << ' ' << arg;
};
(... , outWithSpace(args));
std::cout << '\n';
}
通过使用auto声明的附加模板参数,可以使print()参数化更加灵活,分隔符可以是字符、字符串或任何其他可打印类型。
2.2 支持的运算符
除了.和->以及[]之外,可以对折叠表达式使用所有二元操作符。
- 折叠的函数调用
Fold表达式也可以用于逗号运算符,将多个表达式组合成一个语句。例如,可以折叠逗号运算符,它使您能够执行基类数量可变的成员函数的函数调用:
#include <iostream>
// template for variadic number of base classes
template<typename... Bases>
class MultiBase : private Bases...
{
public:
void print()
{
// call print() of all base classes:
(..., Bases::print());
}
};
struct A
{
void print() { std::cout << "A::print()\n"; }
};
struct B
{
void print() { std::cout << "B::print()\n"; }
};
struct C
{
void print() { std::cout << "C::print()\n"; }
};
int main()
{
MultiBase<A, B, C> mb;
mb.print();
}
结果如下:
这里,
template<typename... Bases>
class MultiBase : private Bases...
{
......
}
允许我们初始化对象的基类数量可变:
MultiBase<A,B,C> mb;
并且
(... , Bases::print());
使用折叠表达式将其展开,以便为每个基类调用print。也就是说,折叠表达式的语句扩展为:
(A::print() , B::print()) , C::print();
但是,请注意,由于逗号运算符的性质,我们使用左折叠运算符还是右折叠运算符并不重要。函数总是从左到右调用。
(Bases::print() , ...);
括号只对调用进行分组,以便第一个print()调用与其他两个print()调用的结果组合在一起,如下所示:
A::print() , (B::print() , C::print());
但是因为逗号运算符的求值顺序总是从左到右仍然是第一个调用A::print()发生在括号内的两个调用组(B::print(), C::print())之前,其中中间调用B::print()仍然发生在右边调用C::print()之前。然而,由于左折叠表达式与结果的计算顺序相匹配,所以当将左折叠表达式用于多个函数调用时,再次建议使用它们。
- 结合哈希函数
使用逗号运算符的一个例子是组合hash值。可以这样做:
#include <iostream>
#include <string>
#include <functional>
template<typename T>
void hashCombine(std::size_t& seed, const T& val)
{
seed ^= std::hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
template<typename... Types>
std::size_t combinedHashValue(const Types& ... args)
{
std::size_t seed = 0; // initial seed
(..., hashCombine(seed, args)); // chain of hashCombine() calls
return seed;
}
int main()
{
std::size_t hash_value = combinedHashValue(std::string("Hello"), std::string("World"), 42);
std::cout << hash_value << std::endl;
return 0;
}
结果如下:
这里
combinedHashValue ("Hello", "World", 42,);
被扩展为:
hashCombine(seed,std::string("Hello")), (hashCombine(seed,std::string("World")), hashCombine(seed,42);
有了这个定义,我们可以很容易地定义一个新的哈希函数对象的类型,如Customer:
struct CustomerHash
{
std::size_t operator() (const Customer& c) const
{
return combinedHashValue(c.getFirstname(), c.getLastname(), c.getValue());
}
};
我们可以用它把Customer放在一个无序的集合里:
std::unordered_set<Customer, CustomerHash> coll;
- 折叠路径遍历
还可以使用一个折叠表达式来遍历操作符为->*的二叉树中的路径:
#include <iostream>
// define binary tree structure and traverse helpers:
struct Node
{
int value;
Node* left;
Node* right;
Node(int i = 0) : value(i), left(nullptr), right(nullptr)
{
}
};
auto left = &Node::left;
auto right = &Node::right;
// traverse tree, using fold expression:
template<typename T, typename... TP>
Node* traverse(T np, TP... paths)
{
return (np ->* ...->*paths); // np ->* paths1 ->* paths2 ...
}
int main()
{
// init binary tree structure:
Node* root = new Node{ 0 };
root->left = new Node{ 1 };
root->left->right = new Node{ 2 };
// traverse binary tree:
Node* node = traverse(root, left, right);
return 0;
}
在这里,
(np ->* ... ->* paths)
使用折叠表达式遍历来自np的路径的可变元素。当调用:
traverse(root, left, right);
折叠表达式的调用扩展为:
root->left->right
结果如下:
2.3 对类型使用折叠表达式
通过使用类型特征,我们还可以使用折叠表达式来处理模板参数包(作为模板参数传递的任意数量的类型)。例如,可以使用折叠表达式来确定类型列表是否同类的:
#include <type_traits>
// check whether passed types are homogeneous:
template<typename T1, typename... TN>
struct IsHomogeneous
{
static constexpr bool value = (std::is_same<T1,TN>::value && ...);
};
// check whether passed arguments have the same type:
template<typename T1, typename... TN>
constexpr bool isHomogeneous(T1, TN...)
{
return (std::is_same<T1,TN>::value && ...);
}
类型特征IsHomegeneous<>可以按照如下使用:
IsHomogeneous<int, MyType, decltype(42)>::value
在这种情况下,初始化成员值的折叠表达式扩展为:
std::is_same<int,MyType>::value && std::is_same<int,decltype(42)>::value
函数模板isHomogeneous<>()可以这样使用:
isHomogeneous(43, -1, "hello", nullptr)
在这种情况下,初始化成员值的折叠表达式扩展为:
std::is_same<int,int>::value && std::is_same<int,const char*>::value
&& std::is_same<int,std::nullptr_t>::value
std::array<>的推导指南在标准库中使用了这个特性。
折叠表达式有写情况下使用起来很方便,例如:
template <typename R, typename ... Ts>
auto matches(const R& range, Ts ... ts)
{
return (std::count(std::begin(range), std::end(range), ts) + ...);
}
辅助函数中使用STL中的 std::count 函数。这个函数需要三个参数:前两个参数定义了迭代器所要遍历的范围,第三个参数则用于与范围内的元素进行比较。 std::count 函数会返回范围内与第三个参数相同元素的个数。在我们的折叠表达式中,我们也会将开始和结束迭代器作为确定范围的参数传入 std::count 函数。不过,对于第三个参数,我们将会每次从参数包中放入一个不同参数。最后,函数会将结果相加返回给调用者。
可以这样使用:
std::vector<int> v{1, 2, 3, 4, 5};
matches(v, 2, 5); // return 2
matches(v, 100, 200); // return 0
matches("abcdefg", 'x', 'y', 'z'); // return 0
matches("abcdefg", 'a', 'b', 'f'); // return 3
如我们所见, matches 辅助函数十分灵活——可以直接传入 vector 或 string 直接调用。其对于初始化列表也同样适用,也适用于 std::list , std::array , std::set 等STL容器的实例。
检查集合中的多个插入操作是否成功
我们完成了一个辅助函数,用于将任意数量参数插入 std::set 实例中,并且返回是否所有插入操作都成功完成:
template <typename T, typename ... Ts>
bool insert_all(T &set, Ts ... ts)
{
return (set.insert(ts).second && ...);
}
可以这样使用它:
std::set<int> my_set{1, 2, 3};
insert_all(my_set, 4, 5, 6); // Returns true
insert_all(my_set, 7, 8, 2); // Returns false, because the 2 collides
需要注意的是,当在插入3个元素时,第2个元素没有插入成功,那么 && 会根据短路特性,终止插入剩余元素:
std::set<int> my_set{1, 2, 3};
insert_all(my_set, 4, 2, 5); // Returns flase,set contains {1, 2, 3, 4} now, without the 5!
检查所有参数是否在范围内
当要检查多个变量是否在某个范围内时,可以多次使用查找单个变量是否在某个范围的方式。这里我们可以使用折叠表达式进行表示
template <typename T, typename ... Ts>
bool within(T min, T max, Ts ...ts)
{
return ((min <= ts && ts <= max) && ...);
}
within(10, 20, 1, 15, 30); // --> false
within(10, 20, 11, 12, 13); // --> true
within(5.0, 5.5, 5.1, 5.2, 5.3) // --> true
将多个元素推入vector中
可以编写一个辅助函数,不会减少任何结果,又能同时处理同一类的多个操作。比如向 std::vector 传入元素:
template <typename T, typename ... Ts>
void insert_all(std::vector<T> &vec, Ts ... ts)
{
(vec.push_back(ts), ...);
}
int main()
{
std::vector<int> v{1, 2, 3};
insert_all(v, 4, 5, 6);
}