notes_from_tmp_in_practice_deep_learning
TMP related notes from book
1.1 Metafunction and type_traits
1.1.1 Introduction to Metafunctions
-
Metafunctions are called and executed at compile time. During the compilation phase, the compiler can only construct constants as intermediate results, unable to construct and maintain variables that can record the state of the system and change it hereafter, so functions (which are metafunctions) used at compile time can only be functions without side effects.
-
Nature of C++ Metaprogramming:
- Functions in C++ metaprogramming are akin to mathematical functions with no side effects.
- Metafunctions must produce the same output for the same input regardless of how many times they are called.
-
Characteristics of Functions with Side Effects:
- Functions with side effects produce different outputs with the same input if there are changes in the system state.
- This behavior contrasts with metafunctions, which are called at compile time and cannot maintain or alter system state.
-
Example of a Metafunction:
constexpr int fun(int a) { return a + 1; }- This function is defined with
constexpr, indicating it can be evaluated at compile time without side effects.
-
Contrast with Non-Metafunction:
-
Example of erroneous code:
static int call_count = 3; constexpr int fun2(int a) { return a + (call_count++); } -
fun2attempts to use a mutating global variable (call_count), leading to a compilation error because it violates the no side effects rule.
-
1.1.2 Type Metafunction
-
Mathematical Function Representation:
- Mathematical functions are usually expressed as y = f(x), involving input (x), output (y), and a mapping (f).
-
Function Input and Output Variability:
- While typically numeric, inputs and outputs of functions can vary, such as mapping events to probabilities in probability theory.
-
Metaprogramming and Metafunctions:
- Core elements of metaprogramming are metafunctions, where both input and output can be various types, not limited to numeric values.
-
Example of Type Metafunction in C++:
-
Metafunction mapping integer types to corresponding unsigned types:
template <typename T> struct Fun_ { using type = T; }; template <> struct Fun_<int> { using type = unsigned int; }; template <> struct Fun_<long> { using type = unsigned long; }; Fun_<int>::type h = 3;
-
-
Understanding Metafunction Definition:
- Metafunctions, like
Fun_, do not resemble regular C++ functions visually but fulfill the criteria of a metafunction:- Input is a type
T. - Output is an internal type from the template,
Fun_<T>::type. - Mapping is executed through template specialization.
- Input is a type
- Metafunctions, like
-
Is Fun a metafunction?
- As defined in the book C++ Template Metaprogramming, it is not at least a "standard metafunction" because it does not have an inline
typedeclaration. - However, according to the discussion at the beginning of this chapter, it is a metafunction because it has input (T), output (
Fun<T>::type), and clearly defines the mapping rules. So, we consider it a metafunction.
- As defined in the book C++ Template Metaprogramming, it is not at least a "standard metafunction" because it does not have an inline
-
Broader Definitions and Examples of Metafunctions:
- Metafunctions are not restricted to functions with
constexpror templates with type declarations. - Inputs and outputs can extend beyond types to include numeric values or templates, enhancing flexibility and decreasing coding limitations.
- Metafunctions are not restricted to functions with
1.1.3 Various Metafunctions
-
Flexibility and Evolution of Metafunctions in C++:
- Metafunctions are not originally part of the C++ language design; they evolved to support operations with "no side effects" and compile-time execution.
- There are no strict rules on the structure of metafunctions, allowing for diverse forms and functionalities.
-
Examples of Metafunctions:
-
Simple template metafunction:
template <typename T> struct Fun {}; -
Parameterless metafunctions:
-
Returns type
int:struct Fun { using type = int; }; -
Returns value
10usingconstexpr:constexpr int fun() { return 10; }
-
-
-
Extended Use of
constexprin Metafunctions:-
C++14 enhances the ability to define metafunctions using
constexprfor simpler and more direct expressions:template <int a> constexpr int fun = a + 1; -
This defines a variable template rather than a traditional function but serves as a metafunction with compile-time evaluation.
-
-
Metafunctions with Multiple Return Types:
-
The ability to output multiple types from a single input exemplifies advanced metafunction capabilities:
template <> struct Fun<int> { using reference_type = int&; using const_reference_type = const int&; using value_type = int; }; -
Despite the complexity, such designs are valid and can be beneficial in specific programming scenarios.
-
1.1.4 type_traits
-
Introduction to
type_traitsLibrary:- Originated from Boost and incorporated into C++11.
- Accessible via the header file
<type_traits>. - Provides a suite of functionalities for type transformation, comparison, and judgment.
-
Usage of Metafunctions in
type_traits:-
Example demonstrating type transformation:
std::remove_reference<int&>::type h1 = 3; // Transforms `int&` to `int` std::remove_reference_t<int&> h2 = 3; // Simplified form using alias template -
std::remove_referenceandstd::remove_reference_tare metafunctions for type manipulation, where the latter is a simplified alias introduced for easier use.
-
-
Role of
type_traitsin Generic Programming:- Essential for writing generic code that requires type transformations.
- Frequently utilized in frameworks, including deep learning frameworks, to handle type-specific logic.
1.1.5 Metafunctions and Macros
-
Macros as Metafunctions:
- While macros can technically be considered metafunctions due to their function-like behavior, they are generally not included in discussions about C++ metafunctions.
- This exclusion is due to macros being processed by the preprocessor, not the compiler, limiting their integration with compile-time features.
-
Limitations of Macros:
- Macros lack namespace support, posing risks of name conflicts with other parts of the code.
- Unlike
constexprfunctions and templates, macros do not support the advanced features available to compiler-processed metafunctions.
-
Use of Macros in Specific Contexts:
- Despite their limitations, macros can be advantageous in certain scenarios, such as in the construction of deep learning frameworks.
- Macros can serve as supplements to template metafunctions, offering utility where templates may not suffice.
-
Guidelines for Using Macros in Frameworks:
- It is recommended to restrict end-user access to macros within the framework to avoid potential misuse.
- Macros should be undefined after their purpose is fulfilled to clean up the namespace and prevent unintended effects.
1.1.6 The Nominating Method of Metafunctions in This Book
In this book, metafunctions are named differently depending on the form of return values for metafunctions:
- if the return value of a metafunction is represented by a dependent name, the function will be named the form of xxx_ (an underscore as the suffix);
- if the return value of a metafunction is represented directly with a non-dependent name, the name of the metafunction will not contain a suffix in the form of an underscore.
template <int a, int b>
struct Add_ {
constexpr static int value = a + b;
};
template <int a, int b>
constexpr int Add = a + b;
constexpr int x1 = Add_<2, 3>::value;
constexpr int x2 = Add<2, 3>;
1.2 Template Template Parameters and Container Templates
- Metafunctions handle three categories of "metadata":
- Numeric values
- Types
- Templates
- The term "metadata" is used to distinguish these elements from "data" manipulated during runtime.
1.2.1 Templates as the Input of Metafunctions
template <template <typename> class T1, typename T2>
struct Fun_ {
using type = typename T1<T2>::type;
};
template <template <typename> class T1, typename T2>
using Fun = typename Fun_<T1, T2>::type;
Fun<std::remove_reference, int&> h = 3;
- The
Funmetafunction is defined to take two parameters: a template (T1) and a type (T2). - It uses the template
T1withT2as an argument, and outputs the result of this application. - Code implementation:
Fun_struct appliesT1<T2>and stores the result as a type.- Alias
Funsimplifies access to this type result.
- Usage example:
Fun<std::remove_reference, int&>results in the typeint.- Variable
his declared as anintand initialized with the value 3.
- In functional programming terms:
Funis a higher-order function because it takes a function (T1, a template) as an input.- Mathematically represented as
Fun(T1, t2) = T1(T2), whereT1is a function andt2is a value.
1.2.2 Templates as the Output of Metafunctions
- Templates can also be used as the output of metafunctions in addition to the input of metafunctions
template <bool AddOrRemoveRef>
struct Fun_;
template <>
struct Fun<true> {
template <typename T>
using type = std::add_lvalue_reference<T>;
};
template <>
struct Fun<false> {
template <typename T>
using type = std::remove_reference<T>;
};
template <typename T>
using Res_ = Fun_<false>::template type<T>;
Res<int&>::type h = 3;
- The
Fun_metafunction is designed to manipulate template types based on a boolean parameter, showing versatility in handling C++ templates:- For the
truecase, it outputs a template that adds an lvalue reference (std::add_lvalue_reference). - For the
falsecase, it outputs a template that removes references (std::remove_reference).
- For the
- Implementation and usage:
- Template specializations for
trueandfalseprovide different behaviors based on the boolean input. Res_usesFun_<false>to apply thestd::remove_referencetemplate toint&, resulting inint.Res<int&>::type h = 3;initializeshas an integer with the value 3.
- Template specializations for
- Mathematical representation:
Fun(addOrRemove) = TwhereaddOrRemoveis a boolean, andTis the output template fromFun_, which itself acts as a metafunction.
1.2.3 "Containers" in Metaprogramming
-
Containers in Metaprogramming:
- Containers, rather than arrays, are essential for holding elements (numeric values, types, templates).
- Containers in metaprogramming typically hold only one category of operands due to simplicity and common requirements.
-
Variadic Templates for Containers:
// 1. container that can hold int
template <int... Vals>
struct IntContainer;
// 2. container that can hold bool
template <bool... Vals>
struct BoolContainer;
// 3. container that can hold types
template <typename... Types>
struct TypeContainer;
// 4. hold templates as its element and each template element can receive one
// type as a parameter
template <template <typename> class... T>
struct TemplateCont;
// 5. take templates as its element, but each template can hold zero or more
// type parameters.
template <template <typename...> class... T>
struct TemplateCont2;
// Note, all 5 above don't have definition. The declaration already contains all
// the information that the compiler needs.
- Metaprogramming Idioms:
- Metaprogramming often uses declarations without definitions unless necessary, reducing complexity and compile-time overhead.
- This approach is illustrated with the declaration of containers without definitions, as they provide sufficient information for the compiler.
1.3.1 Executed in Sequence Order
template <typename T>
struct RemoveReferenceConst_ {
private:
using inter_type = typename std::remove_reference<T>::type;
public:
using type = typename std::remove_const<inter_type>::type;
};
template <typename T>
using RemoveReferenceConst = typename RemoveReferenceConst_<T>::type;
RemoveReferenceConst<const int&> h = 3;
-
Overview of RemoveReferenceConst_ Metafunction:
- The metafunction
RemoveReferenceConst_performs two sequential transformations on the typeTto remove both reference and const qualifiers.
- The metafunction
-
Detailed Breakdown of Operations:
- The metafunction
RemoveReferenceConst_performs two sequential transformations on the typeTto remove both reference and const qualifiers.
- Calculate
inter_type: Removes the reference fromTusingstd::remove_reference<T>::type. - Calculate
type: Removes the const qualifier frominter_typeusing `std::remove_const
- The metafunction
1.3.2 Codes Executed in Branches
Branching at compile time involves creating decision paths based on compile-time conditions, which can dictate the behavior of both compile-time and runtime code.
1.3.2.1 Implementing Branches Using std::conditional and std::conditional_t
namespace std {
template <bool B, typename T, typename F>
struct conditional {
using type = T;
};
template <typename T, typename F>
struct conditional<false, T, F> {
using type = F;
};
template <bool B, typename T, typename F>
using conditional_t = typename conditional<B, T, F>::type;
} // namespace std
// Its logic is that if B is true, the function returns T, otherwise F is
// returned. It is typically used in the following way:
std::conditional<true, int, float> :: type x = 3;
std::conditional t<false, int, float> y =1.0f;
- The advantage of conditional and conditional_t is that they are relatively simple to use, but
- the disadvantage is that they are not very expressive—it can only implement binary branches (true and false branches) and it behaves more like a conditional operator expression at runtime:
x = B? T:F; - Support for multiple branches (a feature similar to switch) is more difficult.
- Accordingly, the use of conditional and conditional_t is relatively narrow. Thus, these two metafunctions are not recommended unless there are particularly simple cases of branches.
1.3.2.2 Implementing Branches with (Partial) Specialization
- Branch Implementation Using Specialization:
- Specialization in templates allows for introducing specific behaviors based on the type. It's a natural and common method to implement branching logic in C++.
struct A;
struct B;
template <typename T>
struct Fun_ {
constexpr static size_t value = 0;
};
template <>
struct Fun_<A> {
constexpr static size_t value = 1;
};
template <>
struct Fun_<B> {
constexpr static size_t value = 2;
};
constexpr size_t h = Fun_<B>::value; // assign h with 2 as T is B
- Legal Constraints on Specialization:
template <typename TW>
struct Wrapper {
template <typename T>
struct Fun_ {
constexpr static size t value = 0;
};
template <>
struct Fun_<int> {
constexpr static size t value = 1;
};
};
-
It's illegal to fully specialize a template within a non-fully specialized class template, as shown in the initial
Wrappercode snippet, where the full specialization ofFun<int>insideWrappercauses a compilation error. -
Solution Using Partial Specialization:
- Introduce a dummy template parameter (
TDummy) to convert full specialization into partial specialization:- This allows for specialization inside templated classes without breaking C++ standards.
- The default value of
TDummyisvoid, allowing the specialized metafunction to be called asFun_<int>without additional parameters.
- Introduce a dummy template parameter (
template <typename TW>
struct Wrapper {
template <typename T, typename TDummy = void>
struct Fun_ {
constexpr static size_t value = 0;
};
template <typename TDummy>
struct Fun _<int, TDummy> {
constexpr static size_t value = 1;
};
};
1.3.2.3 Implementing Branches Using std::enable_if and std::enable_if_t
-
Overview of
std::enable_ifandstd::enable_if_t:enable_ifis a template structure used in C++ to conditionally enable or disable certain template specializations based on a boolean conditionB.- When
Bis true,enable_ifspecializes to enable the typeT; otherwise, it does not provide a type, effectively disabling the template.
-
Code Functionality Using
enable_if:- This technique leverages SFINAE (Substitution Failure Is Not An Error), a feature in C++ that allows template specialization to fail without generating a compile-time error.
- By using
enable_if, developers can conditionally compile code depending on the truth of the template argument.
-
Example with Function Overloading:
namespace std {
template <bool B, typename T = void>
struct enable_if {};
template <class T>
struct enable_if<true, T> {
using type = T;
};
template <bool B, class T = void>
using enable_if_t = typename enable_if<B, T>::type;
} // namespace std
template <bool IsFeedbackOut, typename T,
std::enable_if_t<IsFeedbackOut>* = nullptr>
auto FeedbackOut(T&&) { /* ... */
}
template <bool IsFeedbackOut, typename T,
std::enable_if_t<!IsFeedbackOut>* = nullptr>
auto FeedbackOut_(T&&) { /* ... */
}
-
Two overloads of
FeedbackOutfunction are defined:- The first overload is enabled only if
IsFeedbackOutis true. - The second overload is enabled only if
IsFeedbackOutis false.
- The first overload is enabled only if
-
std::enable_if_tis used as a non-type template parameter defaulted tonullptrto control which overload is active based on theIsFeedbackOutcondition. -
Practical Usage and Implications:
enable_ifandenable_if_tare crucial for creating overloads that cannot be distinguished by parameter types alone, thus solving some overload resolution issues in template-heavy code.- They can be used in various contexts where SFINAE is supported, not limited to template parameters, thus offering flexibility in template function design.
-
Readability and Understanding:
- While powerful, the usage of
enable_ifandenable_if_tcan make the code less intuitive and harder to read compared to direct template specialization. - The complexity introduced by SFINAE might require a deeper understanding of template mechanics, which may not be as straightforward as other methods like template specialization.
- While powerful, the usage of
1.3.2.4 Compile-time Branches with Different Return Types
- Runtime Return Type Limitations:
auto wrap1(bool Check) {
if (Check)
return (int)0;
else
return (double)0;
}
-
In the provided runtime code snippet, there's an attempt to use conditional logic to return different types (
intordouble). However, in C++14, a function without an explicitly declared return type must return the same type from all return statements. The code snippet will fail to compile because it violates this requirement. -
Compile-Time Type Flexibility:
- The limitation of having a single return type in functions is overcome using template metaprogramming and
std::enable_if. - By templating the function
funon a booleanCheckand usingstd::enable_if_t, different implementations offuncan be enabled based on the value ofCheck.
- The limitation of having a single return type in functions is overcome using template metaprogramming and
template <bool Check, std::enable_if_t<Check>* = nullptr>
auto fun() {
return (int)0;
}
template <bool Check, std::enable_if_t<!Check>* = nullptr>
auto fun() {
return (double)0;
}
template <bool Check>
auto wrap2() {
return fun<Check>();
}
int main() { std::cerr << wrap2<true>() << std::endl; }
-
Two template specializations of
funare defined:- The first specialization returns an
intwhenCheckis true. - The second returns a
doublewhenCheckis false.
- The first specialization returns an
-
Use of
wrap2Function:wrap2wraps aroundfun, forwarding its template argumentCheck. The return type ofwrap2is determined by the type returned by the calledfunspecialization, effectively enablingwrap2to also return different types based on the compile-time boolean.
-
Practical Implications and Advantages:
- This method showcases the flexibility of C++ templates to implement functions that can change return types based on compile-time conditions. It is particularly useful in scenarios where different types must be handled based on compile-time conditions.
- This approach enhances metaprogramming capabilities, making it a powerful feature for developing complex, type-safe algorithms and libraries.
-
C++17
if constexpr:- The introduction of
if constexprin C++17 further simplifies compile-time conditional logic by allowing a more straightforward and readable way to implement compile-time branches within the same function body, without the need for separate template specializations. if constexprexecutes conditionally at compile-time, effectively allowing different code branches to be compiled based on the condition, similar to the separate template specializations but within a single function body.
- The introduction of
1.3.2.5 Simplify Codes with if constexpr
-
Introduction to
if constexpr:if constexpris a C++17 feature that allows conditional compilation within function bodies based on compile-time constants. It enables the compiler to conditionally include or exclude code blocks during compilation, enhancing optimization and code simplicity.
-
Functionality of
if constexpr:- The compiler evaluates the condition provided to
if constexprat compile time. If the condition is true, it compiles the code within the corresponding block; otherwise, it ignores that block. - This behavior allows a single function to return different types based on the compile-time condition, simplifying template specialization and reducing code duplication.
- The compiler evaluates the condition provided to
template <bool Check>
auto fun() {
if constexpr (Check)
return (int)0;
else
return (double)0;
}
int main() { std::cerr << fun<true>() << std::endl; }
-
Advantages of
if constexpr:- Reduces the number of function instances the compiler needs to generate compared to separate template specializations or function overloads. This can lead to smaller binary sizes and faster compilation times.
- Simplifies code by avoiding multiple template specializations for each condition, making the code more readable and maintainable.
-
Drawbacks and Limitations:
- Compile-time vs. Runtime: If the
constexprcondition is omitted or not met, the branching logic might inadvertently shift from compile-time to runtime, which reduces the effectiveness ofif constexprand may lead to performance penalties. - Narrow Usage Scope:
if constexpris primarily useful within functions to direct compile-time logic and cannot be used to vary class templates or other metafunctions that require different types based on compile-time conditions. Its use is limited to situations where the function logic diverges based on compile-time known conditions.
- Compile-time vs. Runtime: If the
1.3.3 Codes Executed in Loops
- Traditional loops (while, for) are not used because they require variable operations, incompatible with compile-time operations which focus on constants, types, and templates.
- Recursion is utilized instead of looping constructs to manipulate metadata at compile time.
template <size_t Input>
constexpr size_t OnesCount = (Input % 2) + OnesCount<(Input / 2)>;
template <>
constexpr size_t OnesCount<0> = 0;
constexpr size_t res = OnesCount<45>;
template <size_t ... Inputs>
constexpr size_t Accumulate = 0;
template <size_t CurInput, size_t ... Inputs>
constexpr size_t Accumulate<CurInput, Inputs ... >
= CurInput + Accumulate<Inputs ... >;
constexpr size_t res = Accumulate<1, 2, 3, 4, 5>;
template <size_t... values>
constexpr size t fun() {
constexpr size t res = fun<1, 2, 3, 4, 5>();
return (0 + ... + values);
}
- Fold Expressions in C++:
- Simplified method for writing loops that operate at compile-time.
- Used primarily for operations on parameter packs in template metaprogramming.
- Offers a concise syntax but has specific limitations and use cases.
1.3.4 Caution: Instantiation Explosion and Compilation Crash
1.3.5 Branch Selection and Short Circuit Logic
template <size_t N>
constexpr bool is_odd = ((N % 2) == 1);
template <size_t N>
struct AllOdd_ {
constexpr static bool is_cur_odd = is_odd<N>;
constexpr static bool is_pre_odd = AllOdd_<N - 1>::value;
constexpr static bool value = is_cur_odd && is_pre_odd;
};
template <>
struct AllOdd_<0> {
constexpr static bool value = is_odd<0>;
};
-
Metafunction
is_odd:- Determines if a number
Nis odd. - Defined using a simple modulus operation:
constexpr bool is_odd = ((N % 2) == 1);.
- Determines if a number
-
Recursive Template
AllOdd_:- Recursively evaluates if all numbers from
0toNare odd. - Uses
is_oddto check if the current numberNis odd. - Recursively checks if all previous numbers are odd using
AllOdd_<N - 1>. - Implements logical AND to combine results:
constexpr static bool value = is_cur_odd && is_pre_odd;.
- Recursively evaluates if all numbers from
-
Specialization for Base Case:
- Base case for the sequence when
Nis0, usingAllOdd_<0>to start the recursion. - Ensures that the sequence includes the evaluation of
0being odd or not.
- Base case for the sequence when
-
Compiler Instantiations:
- Each recursive call results in a new instantiation of the
AllOdd_template for each decrement ofNuntil reaching0. - This process results in
N + 1instantiations total, one for each number in the sequence from0toN.
- Each recursive call results in a new instantiation of the
-
Lack of Short Circuiting:
- Despite the logical AND operation, template metaprogramming does not utilize short circuit logic effectively.
- The recursive instantiation proceeds regardless of intermediate results, potentially leading to unnecessary computations if any number in the sequence is not odd.
-
The following is an improved version of the program (only the modified sections are listed here):
template <bool cur, typename TNext>
constexpr static bool AndValue = false;
template <typename TNext>
constexpr static bool AndValue<true, TNext> = TNext::value;
template <size t N>
struct Allodd_ {
constexpr static bool is_cur_odd = is_odd<N>;
constexpr static bool value = AndValue<is_cur_odd, Allodd_<N - 1>>;
};