In this post, you will see how fold-expressions behave when there are no arguments, and also how they behave when there is a single argument.
Fold expressions exist in C++ since C++17 and significantly affect how we treat variadic templates. Back in the day, I wrote about fold-expressions as part of the metaprogramming series, but today, we will explore the extreme cases of fold-expression usages.
An important disclaimer before we start: In this article, code examples show variadic template usages with arguments passed by value, without forwarding them. This is done to simplify them and to focus on the idea behind the examples.
Empty Variadic Parameters
In the case of a unary fold (fold expression without initialization), this case is legal for 3 types of operators: &&
, ||
, and ,
.
Operator &&
template <typename ...Args>
auto and_cond(Args... args) {
return (args && ...);
}
In case of empty parameters (for the call and_cond()
), the function will return true
. A reasonable explanation for this decision might be that &&
operator requires there won’t be any part that evaluates false
. In this case, there are no parts at all, so none of the parts evaluate false
, and therefore the result should be true
.
Operator ||
template <typename ...Args>
auto or_cond(Args... args) {
return (args || ...);
}
In case of empty parameters, the function will return false
. A reasonable explanation for this decision might be that ||
operator requires that at least one of the parts will evaluate true
. Because there are no parts at all, none of them evaluates true
, therefore the result is false
.
Operator ,
template <typename ...Args>
auto comma_op(Args... args) {
return (args , ...);
}
For this case, when there are no parameters passed, the result will be void()
.
Other Operators
For other operators, any call without parameters won’t compile.
Variadic Pack with a Single Parameter
Here, things get a little bit unexpected. When passing only one parameter to a variadic pack, the result will ignore the operator and return the same type & parameter sent to the function (tested on both gcc 13.1.0 & clang 16.0.0). For example:
template <typename ...Args>
auto func(Args ...args)
{
return (args || ...);
}
int main() {
using namespace std::string_literals;
std::cout << func("I am a string"s); return EXIT_SUCCESS;
}
The generated code according to C++ Insights is:
template<>
std::basic_string<char> func<std::basic_string<char>>(std::basic_string<char> __args0)
{
return std::basic_string<char>(static_cast<std::basic_string<char>&&>(__args0));
}
The same would be for any other operator, including +=
, .*
and ->
.
Binary Fold Expression without Parameters
The same rules for a unary fold expression with a single parameter are applied to a binary fold expression without parameters:
template <typename ...Args>
auto func(Args ...args)
{
return (args ->* ... ->* "I am a string"s);
}
int main() {
std::cout << func(); return EXIT_SUCCESS;
}
C++ Insights generated code:
template<>
std::basic_string<char> func<>()
{
return std::operator""s("I am a string", 13UL);
}
Conclusion
We have to pay close attention to extreme cases and can never count on basic logic to be on our side (who would believe that we can send a std::string
to a fold expression that uses ||
or &&
operators?).
This article was inspired by a conversation between Daisy Hollman and me. Thank you Daisy!
An unrelated note: My articles’ publish rate decreased during the last several months. The reason is the preparations for Daisy’s and my talk in Core C++ (guess which conversation led to this article).