C++ For C# Developers: Part 34 – Fold Expressions and Elaborated Type Specifiers
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrap-up
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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:
+
-
*
/
%
^
&
|
=
<
>
<<
>>
+=
-=
*=
/=
%=
^=
&=
|=
<<=
>>=
==
!=
<=
>=
&&
||
,
.*
->*
Elaborated Type Specifiers
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;
Conclusion
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.