

C++ For C# Developers: Part 34 – Fold Expressions and Elaborated Type Specifiers
source link: https://www.jacksondunstan.com/articles/6237
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C++ For C# Developers: Part 34 – Fold Expressions and Elaborated Type Specifiers
January 4, 2021 Tags: generic, operator, template, type
Today we’ll cover a couple of more minor features that don’t have C# equivalents: fold expressions and elaborated type specifiers. Though they are small, they can be quite useful!
Table of Contents
Fold Expressions
Fold expressions, available since C++17, allow us to apply a binary operator to all the parameters in a template’s parameter pack. For a simple example, say we want to add up some integers:
// Template parameters are a pack of integers template<int... Vals> // Apply the + operator to the Vals pack int SumOfAll = (Vals + ...); // Instantiate the template with four integers in the Vals pack DebugLog(SumOfAll<1, 2, 3, 4>); // 10
The “fold expression” is the (Vals + ...)
part. Parentheses are required here, unlike most expressions. We name the parameter pack (Vals
), name the binary operator (+
), and add ...
to indicate that we want to fold that operator over the pack.
When the template is instantiated, the compiler converts the fold expression into a series of binary operators:
// Expanded version of the template parameter pack template<int Val1, int Val2, int Val3, int Val4> // Expanded version of the fold expression int SumOfAll = Val1 + (Val2 + (Val3 + Val4));
This kind of fold expression is called a “unary right fold.” This means that the rightmost arguments have the operator applied to them fist.
In order to reverse this and apply the operator to the leftmost arguments first, we use a “unary left fold” like this:
template<int... Vals> int SumOfAll = (... + Vals); // Swapped "..." and "Vals" DebugLog(SumOfAll<1, 2, 3, 4>); // 10
When instantiated, the compiler produces the equivalent of this:
template<int Val1, int Val2, int Val3, int Val4> int SumOfAll = ((Val1 + Val2) + Val3) + Val4;
The choice of a left or right fold doesn’t really matter when we’re just adding integers, but it will surely matter with other types and other operators.
If the parameter pack happens to be empty, only three binary operators are allowed. First, we can use &&
to evaluate to true
:
template<bool... Vals> bool AndAll = (... && Vals); DebugLog(AndAll<false, false>); // false DebugLog(AndAll<false, true>); // false DebugLog(AndAll<true, false>); // false DebugLog(AndAll<true, true>); // true DebugLog(AndAll<>); // true
Second, we can use ||
to evaluate to false
:
template<bool... Vals> bool OrAll = (... || Vals); DebugLog(OrAll<false, false>); // false DebugLog(OrAll<false, true>); // true DebugLog(OrAll<true, false>); // true DebugLog(OrAll<true, true>); // true DebugLog(OrAll<>); // false
And third, which is by far the least common use case, the ,
operator will evaluate to void()
:
template<bool... Vals> void Goo() { return (... , Vals); // Equivalent to "return void();" } // OK Goo();
Now that we’ve seen the “unary” fold expressions, let’s look at the “binary” ones. To make these, we add the same binary operator after the ...
then an additional value:
template<int... Vals> // Add the operator (+) then an additional value (1) after the unary fold int SumOfAllPlusOne = (Vals + ... + 1); DebugLog(SumOfAllPlusOne<1, 2, 3, 4>); // 11
Here we’ve converted the unary fold expression (Vals + ...)
into a binary one by adding + 1
to the end of it. This adds another value in addition to the values in the parameter pack. Since this was a “binary right fold” the parentheses will be added on the rightmost values first:
template<int Val1, int Val2, int Val3, int Val4> int SumOfAll = 1 + (Val1 + (Val2 + (Val3 + Val4)));
The “binary left fold” version just has the additional value on the left:
template<int... Vals> int SumOfAllPlusOne = (1 + ... + Vals);
When instantiated with four values in the parameter pack, it’ll look like this:
template<int Val1, int Val2, int Val3, int Val4> int SumOfAll = (((1 + Val1) + Val2) + Val3) + Val4;
Regardless of which kind of fold expression we write, we’re allowed to use any of these binary operators:
+
-
*
/
%
^
&
|
=
<
>
<<
>>
+=
-=
*=
/=
%=
^=
&=
|=
<<=
>>=
==
!=
<=
>=
&&
||
,
.*
->*
We’ve seen before that C code requires us to use struct Player
instead of just Player
as the type name of the Player
struct:
// C code struct Player { int Health; int Speed; }; struct Player p; // C requires "struct" prefix p.Health = 100; p.Speed = 10; DebugLog(p.Health, p.Speed); // 100, 10
That’s usually not necessary in C++. However, there is an edge case where we have both a class and a variable with the same name. Using the name refers to the variable, so we’re unable to use the type anymore:
// A class struct Player { }; // A variable with the same name as the class int Player = 123; // Compiler error: Player is not a type // This is because "Player" refers to the variable, not the class Player p;
To get around this, we can use the C-style struct Player
to explicitly state that we’re referring to the struct, not the variable. This is called an “elaborated type specifier” since we are elaborating on the Player
type:
// Elaborated type specifier // OK: refers to the Player struct, not the Player variable struct Player p;
Since struct
and class
are very similar, we can use them interchangeably in our elaborated type specifiers:
// Elaborated type specifier using "class" when Player is a "struct" class Player p;
Unions are not interchangeable:
// A union union IntFloat { int32_t Int; float Float; }; // A variable with the same name as the union bool IntFloat = true; // Compiler error: IntFloat is not a type // This is because "IntFloat" refers to the variable, not the union IntFloat u; // Elaborated type specifier // Compiler error: IntFloat is a union, not a class or struct class IntFloat u; // Elaborated type specifier // OK: refers to the IntFloat union, not the IntFloat variable union IntFloat u;
Enumerations are also their own kind of entity and need to be elaborated with enum
:
// An enumeration enum DamageType { Physical, Water, Fire, Magic, }; // A variable with the same name as the enum float DamageType = 3.14f; // Elaborated type specifier // Compiler error: DamageType is an enum, not a class or struct class DamageType d; // Elaborated type specifier // OK: refers to the DamageType enum, not the DamageType variable enum DamageType d;
Plain enum
can be used with a scoped enumeration but enum class
or enum struct
can’t be used with unscoped enumerations and must be used with scoped enumerations:
enum class Scoped { }; enum Unscoped { }; enum Scoped e1; // OK enum Unscoped e2; // OK enum class Scoped e3; // OK enum class Unscoped e4; // Compiler error: can't use scoped with unscoped enum enum struct Scoped e5; // OK enum struct Unscoped e6; // Compiler error: can't use scoped with unscoped enum
Regardless of the type, we can also refer to its location in a namespace using the scope resolution operator:
namespace Gameplay { enum DamageType { Physical, Water, Fire, Magic, }; float DamageType = 3.14f; } // Elaborated type specifier using scope resolution operator enum Gameplay::DamageType d;
The same goes for classes’ member types:
struct Gameplay { // Member type of the class enum DamageType { Physical, Water, Fire, Magic, }; // Member variable of the class constexpr static float DamageType = 3.14f; }; // Elaborated type specifier referring to class member type enum Gameplay::DamageType d;
Fold expressions provide a way for us to cleanly apply binary operators to templates’ parameter packs. Without them, we’d need to resort to alternatives such as recursively instantiating templates and using specialization to stop the recursion. That’s much less readable and much slower to compile as many templates would need to be instantiated and then then thrown away. We get our choice of unary or binary and left or right folds so we can control how the binary operator is applied to the values of the parameter pack. Since C# doesn’t have variadic templates, it also doesn’t have fold expressions.
Elaborated type specifiers are a minor feature that provides us a workaround in the case where we have types with the same name as variables. We can refer to these types explicitly to change the default meaning of the name shared between the type and the variable. This is rarely necessary, but a nice tool to have when the situation arises. C# doesn’t allow a type to have the same name as a variable, so there’s no equivalent to this in that language.
Recommend
-
32
Raymond August 30th, 2019 Windows adopted Unicode before most other operating systems. [citation ne...
-
13
Type Specifiers in C, Part 3mikeash.com: just this guy, you know? Friday Q&A 2009-07-10: Type Specifiers in C, Part 3 by Mike Ash Here at la...
-
6
Type Specifiers in C, Part 2mikeash.com: just this guy, you know? Friday Q&A 2009-07-03: Type Specifiers in C, Part 2 by Mike Ash Welcome to...
-
7
Home > Cpp > C++ Program to maintain a Cricket Team by using CLASS with PUBLIC & PRIVATE Access Specifiers – Q9 C...
-
7
Home > Cpp > C++ Program to implement Toll Tax Problem by using CLASS Access Specifiers and SWITCH CASE – Q12 C++ Pro...
-
7
Copy link Contributor FabianWolff commented
-
11
Home > Cpp > C++ Program to maintain Student Result by using CLASS with PUBLIC & PRIVATE Access Specifiers – Q7
-
16
Home > Cpp > C++ Program to work with Purchase Order Problem by using Pointers and CLASS with PUBLIC & PRIVATE Access Spec...
-
9
Conversation Contributor...
-
3
Conversation Contributor...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK