阅读更多
本文转载摘录自浅谈 C++ 元编程
1 Introduction
1.1 What is Metaprogramming
Metaprogramming calculates the constants, types, and code needed at runtime by manipulating program entities at compile time.
In general programming, a program is directly written, compiled by a compiler, which produces target code for execution at runtime. Unlike regular programming, metaprogramming uses the template mechanism provided by the language, allowing the compiler to deduce and generate programs at compile time. The program deduced by metaprogramming is then further compiled by the compiler to produce the final target code.
Thus, metaprogramming is also known as two-level programming, generative programming, or template metaprogramming.
1.2 The Position of Metaprogramming in C++
C++
Language = Superset of C Language + Abstraction Mechanisms + Standard Library
C++
has two main abstraction mechanisms:
- Object-Oriented Programming
- Template Programming
To support object-oriented programming, C++
provides classes that allow new types to be constructed from existing types in C++
. In the area of template programming, C++
provides templates, which represent general concepts in an intuitive way.
There are two main applications of template programming:
- Generic Programming
- Metaprogramming
The former focuses on the abstraction of general concepts, designing generic types or algorithms without needing to be overly concerned about how the compiler generates specific code.
The latter focuses on selection and iteration during template deduction, designing programs using template techniques.
1.3 History of C++ Metaprogramming
1.4 Language Support for Metaprogramming
C++
metaprogramming primarily relies on the template mechanism provided by the language. In addition to templates, modern C++
also allows the use of constexpr
functions for constant calculations. Due to the limited functionality of constexpr
functions, recursion depth and the number of calculations are still constrained by the compiler, and compile-time performance is relatively poor. Therefore, most current metaprogramming programs are based on templates. This section mainly summarizes the foundational language features related to the C++
template mechanism, including narrowly defined templates and generic lambda
expressions.
1.4.1 Narrow Templates
The latest version of C++
currently divides templates into four categories:
- Class templates
- Function templates
- Alias templates
- Variable templates
The first two can generate new types and are considered type constructors, while the latter two are shorthand notations added by C++
to simplify the former.
Class templates and function templates are used to define classes and functions with similar functionalities, abstracting types and algorithms in generic programming. In the standard library, containers and functions are applications of class templates and function templates.
Alias templates and variable templates were introduced in C++11
and C++14
, respectively, providing shorthand notation for type aliases and constants with template features. The former can implement methods such as nested classes in class templates, while the latter can be achieved through constexpr
functions, static members of class templates, and function template return values. For example, the alias template std::enable_if_t<T>
in C++14
is equivalent to typename std::enable_if<T>::type
, and the variable template std::is_same<T, U>
in C++17
is equivalent to std::is_same<T, U>::value
. Although these two types of templates are not necessary, they can enhance code readability and improve template compilation performance.
There are three types of template parameters in C++
:
- Value parameters
- Type parameters
- Template parameters
Since C++11
, C++
has supported variadic templates, where the number of template parameters can be indefinite. Variadic parameters are folded into a parameter pack, and during usage, each parameter is iterated at compile time. The standard library’s tuple, std::tuple
, is an application of variadic templates (the tuple’s type parameters are variable-length and can be matched using template<typename... Ts>
).
Although template parameters can be passed as general type parameters (a template is also a type), they are distinguished separately because they allow parameter matching for the passed-in template. Code 8 uses std::tuple
as a parameter, and through matching, extracts the variadic parameters inside std::tuple
.
Specialization is similar to function overloading, providing a template implementation for all template parameter values or some template parameter values. Instantiation is akin to function binding, where the compiler determines which overload to use based on the number and type of parameters. Since functions and templates have similarities in overloading, their parameter overloading rules are also similar.
1.4.2 Generic Lambda Expressions
Since C++
does not allow templates to be defined within functions, sometimes it’s necessary to define a template outside a function to achieve specific local functionality within the function. On one hand, this leads to a loose code structure, making maintenance difficult; on the other, using templates requires passing specific context, which reduces reusability (similar to the callback mechanism in C, where a callback function cannot be defined within a function and must pass context through parameters).
To address this, C++14
introduced generic lambda expressions, which, on one hand, allow constructing closures within functions like the lambda
expressions introduced in C++11
, avoiding the need to define local functionality used within the function outside the function. On the other hand, they enable the functionality of function templates, allowing parameters of any type to be passed.
2 Basic Operations of Metaprogramming
The template mechanism in C++
only provides a pure functional approach, meaning it does not support variables, and all deduction must be completed at compile time. However, the templates provided in C++
are Turing complete (turing complete
), so it is possible to use templates to implement full metaprogramming.
There are two basic calculus rules for metaprogramming:
- Compile-time testing
- Compile-time iteration
These respectively implement selection and iteration in control structures. Based on these two basic calculus methods, more complex calculations can be achieved.
Additionally, metaprogramming often uses template parameters to pass different policies, allowing for dependency injection and inversion of control. For example, std::vector<typename T, typename Allocator = std::allocator<T>>
allows passing an Allocator
to implement custom memory allocation.
2.1 Compile-Time Testing
Compile-time testing is equivalent to the selection statement in procedural programming, allowing the implementation of if-else/switch
selection logic.
Before C++17
, compile-time testing was achieved through template instantiation and specialization, where the most specific template would be matched each time. C++17
introduced a new method for compile-time testing using constexpr-if
.
2.1.1 Testing Expressions
Similar to static assertions, the object of compile-time testing is a constant expression, an expression whose result can be determined at compile time. By using different constant expressions as parameters, various required template overloads can be constructed. For example, Code 1 demonstrates how to construct a predicate isZero<Val>
to determine at compile time whether Val
is 0
.
Code 1:
1 | template <unsigned Val> |
2.1.2 Testing Types
In many metaprogramming applications, type testing is required, where different functionality is implemented for different types. Common type tests fall into two categories:
- Checking if a type is a specific type:
- This can be achieved directly through template specialization.
- Checking if a type meets certain conditions:
- This can be done through the “Substitution Failure Is Not An Error” rule (
SFINAE
), which ensures optimal matching. - Tag dispatch can also be used to match enumerable finite cases (for example,
std::advance<Iter>
selects an implementation based onstd::iterator_traits<Iter>::iterator_category
for the iterator typeIter
).
- This can be done through the “Substitution Failure Is Not An Error” rule (
To better support SFINAE
, C++11
’s <type_traits>
provides predicate templates is_*/has_*
for type checking and two additional helpful templates:
std::enable_if
converts condition checks into constant expressions, similar to the “Test Expression” section’s overload selection (though it requires an extra function parameter, function return type, or template parameter).std::void_t
directly checks for the existence of dependent members/functions; if they do not exist, overload resolution fails (it can be used to construct predicates, then conditions can be checked withstd::enable_if
).
For type-specific checks, similar to Code 1, change unsigned Val
to typename Type
and convert the template parameter from a value parameter to a type parameter, then match the overload according to the optimal match principle.
For condition-based type checks, Code 2 demonstrates how to convert basic C language data types to std::string
in the ToString
function. The code is divided into three parts:
- First, three variable templates
isNum/isStr/isBad
are defined, each corresponding to a predicate for three type conditions (usingstd::is_arithmetic
andstd::is_same
from<type_traits>
). - Then, based on the
SFINAE
rule,std::enable_if
is used to overload the functionToString
, each corresponding to numerical, C-style string, and invalid types. - In the first two overloads,
std::to_string
andstd::string
constructors are called, respectively; in the last overload, a static assertion immediately raises an error.
Code 2:
1 | template <typename T> |
According to the rule of two-phase name lookup, directly using static_assert(false)
would cause a compilation failure in the first phase, before the template is instantiated. Therefore, a type-dependent false
expression (typically dependent on the parameter T
) is required for a failing static assertion.
Similarly, a variable template can be defined as template <typename...> constexpr bool false_v = false
, and false_v<T>
can be used in place of sizeof(T) == 0
.
2.1.3 Using if for Compile-Time Testing
For those new to metaprogramming, it’s common to try using an if
statement for compile-time testing. Code 3 is an incorrect version of Code 2, which serves as a representative example of the differences between metaprogramming and regular programming.
Code 3:
1 | template <typename T> |
The error in Code 3 lies in the function ToString
’s compilation. For a given type T
, the function needs to perform two function bindings: val
is passed as an argument to both std::to_string(val)
and std::string(val)
, and then a static assertion checks whether !isBad<T>
is true
. This causes an issue: one of the two bindings will fail. For instance, if ToString("str")
is called, during compilation, std::string(const char *)
can be correctly overloaded, but std::to_string(const char *)
cannot find a proper overload, leading to a compilation failure.
If this were a scripting language, this code would be fine because scripting languages lack the concept of compilation; all function bindings are performed at runtime. However, in a statically-typed language, function binding is completed at compile time. To allow Code 3’s style to be used in metaprogramming, C++17
introduced constexpr-if
, where simply replacing if
with if constexpr
in Code 3 allows it to compile.
The introduction of constexpr-if
makes template testing more intuitive, improving the readability of template code. Code 4 demonstrates how to use constexpr-if
to solve the issue of compile-time selection. Additionally, the catch-all statement no longer requires the isBad<T>
predicate template and can use a type-dependent false
expression for a static assertion (though a direct static_assert(false)
cannot be used).
Code 4:
1 | template <typename T> |
However, the idea behind constexpr-if
had already appeared as early as Visual Studio 2012
. It introduced the __if_exists
statement, which was used for compile-time testing to check whether an identifier exists.
2.2 Compile-Time Iteration
Compile-time iteration is similar to loop statements in procedural programming, allowing logic similar to for/while/do
loops.
Before C++17
, unlike in regular programming, metaprogramming calculus rules were purely functional, meaning compile-time iteration could not be achieved through variable iteration and instead had to rely on recursion combined with specialization. The general approach is to provide two types of overloads: one that accepts arbitrary parameters and recursively calls itself internally, and another that is a template specialization or function overload of the former, directly returning the result, effectively serving as the termination condition for recursion. Their overload conditions can be either expressions or types.
C++17
introduced fold expressions to simplify the syntax for iteration.
2.2.1 Iterating over Fixed-Length Templates
Code 5 demonstrates how to use compile-time iteration to compute the factorial (N!
) at compile time. The function _Factor
has two overloads: one for any non-negative integer and another for 0
as the parameter. The former uses recursion to produce results, while the latter directly returns the result. When _Factor<2>
is called, the compiler expands it to 2 * _Factor<1>
, then _Factor<1>
expands to 1 * _Factor<0>
, and finally _Factor<0>
directly matches the overload with 0
as the parameter.
Code 5:
1 | template <unsigned N> |
2.2.2 Iterating over Variable-Length Templates
To iterate through each parameter in a variadic template, compile-time iteration can be used to implement loop traversal. Code 6 implements a function that sums all parameters. The function Sum
has two overloads: one for when there are no function parameters, and another for when there is at least one function parameter. Similar to iteration with fixed-length templates, this is also achieved through recursive calls to traverse the parameters.
Code 6:
1 | template <typename T> |
2.2.3 Simplifying Compile-Time Iteration with Fold Expressions
When variadic templates were introduced in C++11
, direct syntax for expanding parameter packs within templates was supported; however, this syntax only allowed unary operations on each parameter within the pack. To perform binary operations between parameters, additional templates were necessary (for example, Code 6 defines two Sum
function templates, with one expanding the parameter pack to recursively call itself).
C++17
introduced fold expressions, enabling direct traversal of each parameter within a parameter pack and applying a binary operator to perform either a left fold or right fold. Code 7 improves upon Code 6 by using a left fold expression with an initial value of 0
.
Code 7:
1 | template <typename... Ts> |
2.3 Metaprogramming vs. Regular programming
Concept | Regular Programming | Metaprogramming |
---|---|---|
Sequence | Statements in order | Nested templates, Type deduction |
Branching | if , else , switch |
std::conditional , constexpr if , SFINAE , template specialization |
Looping (Iteration) | for , while , do-while |
Recursive templates, template specialization, std::integer_sequence |
3 Basic Applications of Metaprogramming
Metaprogramming enables the design of type-safe and runtime-efficient programs with ease. Today, metaprogramming is widely applied in C++
programming practices. For example, Todd Veldhuizen
proposed a metaprogramming approach to construct expression templates, optimizing expressions to improve the runtime speed of vector calculations. Additionally, K. Czarnecki
and U. Eisenecker
used templates to implement a Lisp
interpreter.
Although metaprogramming applications vary, they are combinations of three fundamental types: numeric computation, type deduction, and code generation. For instance, in the ORM
(object-relation mapping) designed by BOT Man
, type deduction and code generation are primarily used. Based on an object’s type in C++
, the types of each field in the corresponding database relation tuple are deduced. Operations on C++
objects are mapped to corresponding database statements, generating the relevant code.
3.1 Numerical Computation
As one of the earliest applications of metaprogramming, numeric computation can be used for compile-time constant calculation and optimizing runtime expression evaluation.
Compile-time constant calculation allows programmers to use the programming language to define constants at compile time, rather than directly writing constants (magic numbers) or calculating these constants at runtime. For example, Codes 5, 6, and 7 perform compile-time constant calculations.
The earliest concept of using metaprogramming to optimize expression evaluation was proposed by Todd Veldhuizen
. By utilizing expression templates, it is possible to implement features such as partial evaluation, lazy evaluation, and expression simplification.
3.2 Type Deduction
Beyond basic numeric computation, metaprogramming can also be used to deduce conversions between arbitrary types. For example, when combining a domain-specific language natively with C++
, type deduction can convert types in these languages into C++
types while ensuring type safety.
BOT Man
proposed a method for compile-time tuple type deduction in SQL
. Since all data types in C++
cannot be NULL
, whereas SQL
fields can be NULL
, fields that may be null are stored in C++
using the std::optional
container. For tuples resulting from SQL
outer joins, where all fields can be NULL
, ORM
needs a method to convert tuples with fields that may be either std::optional<T>
or T
into a new tuple where all fields are std::optional<T>
.
Code 8:
- Define
TypeToNullable
, and specialize it forstd::optional<T>
. Its purpose is to automatically convert bothstd::optional<T>
andT
tostd::optional<T>
. - Define
TupleToNullable
, which decomposes all types in a tuple, converts them into a parameter pack, passes each type in the parameter pack toTypeToNullable
, and finally reassembles the results into a new tuple.
1 | template <typename T> |
3.3 Code Generation
Like generic programming, metaprogramming is often used for code generation. However, unlike simple generic programming, code generated by metaprogramming is often derived through compile-time testing and compile-time iteration. For example, Code 2 generates code that converts basic C language types to std::string
.
In real projects, we often need to convert between C++
data structures and domain models related to actual business logic. For example, a JSON
string representing a domain model might be deserialized into a C++
object, further processed by business logic, and then serialized back into a JSON
string. Such serialization/deserialization code generally does not need to be manually written and can be automatically generated.
BOT Man
proposed a method based on compile-time polymorphism that defines a schema for the domain model, automatically generating code for serialization/deserialization between the domain model and C++
objects. This allows business logic developers to focus more on handling the business logic without needing to worry about low-level data structure conversions.
4 Key Challenges in Metaprogramming
Despite the rich capabilities of metaprogramming, both learning and using it are quite challenging. On one hand, the complex syntax and calculus rules often deter beginners; on the other, even experienced C++
developers can fall into the hidden pitfalls of metaprogramming.
4.1 Complexity
Due to significant language-level limitations in metaprogramming, much metaprogramming code relies heavily on compile-time testing and iteration techniques, often resulting in poor readability. Additionally, designing compile-time calculus in an elegant manner is challenging, making the writability of metaprogramming less favorable compared to typical C++
programs.
Modern C++
continuously introduces features aimed at reducing the complexity of metaprogramming:
- Alias templates in
C++11
provide a shorthand for types within templates. - Variable templates in
C++14
offer a shorthand for constants within templates. constexpr-if
inC++17
introduces a new syntax for compile-time testing.- Fold expressions in
C++17
simplify the process of writing compile-time iterations.
Based on the generic lambda
expressions in C++14
, Louis Dionne
designed the metaprogramming library Boost.Hana
, which proposes metaprogramming without templates, marking the transition from template metaprogramming to modern metaprogramming. The core idea is that using only the generic lambda
expressions in C++14
and constexpr/decltype
from C++11
enables the quick implementation of basic metaprogramming calculus.
4.2 Instantiation Errors
Template instantiation differs from function binding: before compilation, the former imposes few restrictions on the types of parameters passed in, whereas the latter determines the expected parameter types based on the function declaration. Parameter checks for templates occur during instantiation, making it difficult for the program designer to detect potential errors before compilation.
To reduce potential errors, Bjarne Stroustrup
and others proposed introducing concepts at the language level for templates. With concepts, restrictions can be placed on parameters, allowing only types that meet specific requirements to be passed into the template. For example, the template std::max
could be constrained to accept only types that support the <
operator. However, for various reasons, this language feature was not included in the C++
standard for a long time (though it may have been added in C++20
). Despite this, compile-time testing and static assertions can still provide checks.
Additionally, in the case of template instantiation errors at deeper levels, the compiler reports each level of instantiation, leading to verbose error messages that can obscure the source of the issue. BOT Man
proposed a short-circuit compiling method to offer more user-friendly compile-time error messages in metaprogramming libraries. This approach involves having the interface check whether the passed parameters support the required operations before the implementation performs them. If they do not, the interface uses short-circuiting to redirect to an error-reporting interface, stopping compilation and using static assertions to provide error messages. Paul Fultz II
proposed a similar approach to concept/constraint interface checks (like those in C++20
) by defining trait templates corresponding to concepts and checking if these traits are met before usage.
4.3 Code Bloat
Since templates instantiate for each unique set of template arguments, a large number of parameter combinations can lead to code bloat, resulting in a massive codebase. This code can be divided into two types: dead code and effective code.
In metaprogramming, the focus is often on the final result rather than the process. For example, in Code 5, we only care that Factor<4> == 24
and do not need the temporary templates generated during intermediate steps. However, when N
is large, compilation produces many temporary templates. These temporary templates are dead code, meaning they are not executed. The compiler automatically optimizes the final code generation by removing these unused codes at link-time, ensuring that the final output does not include them. Nevertheless, generating excessive dead code wastes valuable compilation time.
In other cases, the expanded code is all effective code—meaning it is executed—but the code size remains large due to the wide variety of parameter types needed. The compiler has limited ability to optimize such code, so programmers should design to avoid code bloat. Thin templates are typically used to reduce the size of template instances; the approach is to abstract common parts of templates instantiated with different parameters into shared base classes or functions, with the distinct parts inheriting from the base class or calling the function to enable code sharing.
For example, in the implementation of std::vector
, T*
and void*
are specialized. Then, the implementation for all T*
types is inherited from the void*
implementation, with public functions using casting to convert between void*
and T*
. This allows all pointer types in std::vector
to share a single implementation, avoiding code bloat (Code 9).
Code 9:
1 | template <typename T> |
4.4 Compile-Time Performance
Although metaprogramming does not add runtime overhead, excessive use can significantly increase compilation time, especially in large projects. Optimizing metaprogramming compile-time performance requires special techniques.
According to the One Definition Rule, a template can be instantiated with the same parameters across multiple translation units and merged into a single instance at link time. However, template operations in each translation unit are independent, which increases compilation time and produces excessive intermediate code. Explicit instantiation is commonly used to avoid repeated template instantiations. The approach is to explicitly define a template instance in one translation unit and declare the same instance with extern
in other translation units. This method, which separates interface from implementation, is also commonly used for template interfaces in static libraries.
Chiel Douwes
conducted an in-depth analysis of common template operations in metaprogramming, comparing the costs of several template operations (Cost of operations: The Rule of Chiel) from highest to lowest (without considering C++14
variable templates):
- Substitution Failure Is Not An Error (
SFINAE
) - Instantiating function templates
- Instantiating class templates
- Using alias templates
- Adding parameters to class templates
- Adding parameters to alias templates
- Using cached types
Following these principles, Odin Holmes
designed the type manipulation library Kvasir
, which achieves high compilation performance compared to type manipulation libraries based on C++98/11
. To measure the effectiveness of compilation performance optimizations, Louis Dionne
developed a CMake-based compile-time benchmarking framework.
Additionally, Mateusz Pusz
shared some best practices for metaprogramming performance. For example, std::conditional_t
based on C++11
alias templates and std::is_same_v
based on C++14
variable templates are faster than the traditional std::conditional/std::is_same
approach. Code 10 demonstrates implementations using std::is_same
and the variable template-based std::is_same_v
.
Code 10:
1 | // traditional, slow |
4.5 Debugging Templates
The primary runtime challenge in metaprogramming is debugging template code. When debugging code that has undergone extensive compile-time testing and compile-time iteration—meaning the code is a concatenation of various templates with many levels of expansion—one must frequently switch back and forth between template instances during debugging. In such cases, it is difficult for the debugger to pinpoint the issue within the expanded code.
As a result, some large projects avoid complex code generation techniques and instead use traditional code generators to produce repetitive code that is easier to debug. For example, Chromium
’s common extension API defines JSON/IDL
files, which a code generator uses to produce the relevant C++
code and simultaneously generate interface documentation.
5 Summary
The emergence of C++
metaprogramming was a serendipitous discovery: people realized that the template abstraction mechanism provided by C++
could be effectively applied to metaprogramming. With metaprogramming, it’s possible to write type-safe and runtime-efficient code. However, excessive use of metaprogramming can increase compilation time and reduce code readability. Nevertheless, as C++
continues to evolve, new language features are consistently introduced, offering more possibilities for metaprogramming.
6 Applications of Metaprogramming
6.1 Utility
6.1.1 is_same
1 | template <typename T, typename U> |
6.1.2 rank
1 |
|
We can also use std::integral_constant
to achieve the functionality described above.
1 |
|
6.1.3 one_of/type_in/value_in
Here is the implementation approach for is_one_of
:
- First, define the template.
base1
: Define the single-parameter instantiation version, which serves as the termination state for recursion.base2
: Define the instantiation version where the first element matches, also serving as a termination state for recursion.- Define the instantiation version where the first element differs, and implement recursive instantiation through inheritance (during recursion, be mindful to reduce one parameter with each step).
1 |
|
We can also use fold expressions to implement recursive expansion.
1 |
|
6.1.4 is_copy_assignable
Let’s manually implement std::is_copy_assignable
from the <type_traits>
header, which is used to check if a class supports the copy assignment operator.
Example implementation steps and explanation:
std::declval<T>
is used to return aT&&
type (refer to the reference collapsing rules).- The function template
try_assignment(U&&)
has two type parameters, where the second type parameter is unused (its name is omitted) and has a default value oftypename = decltype(std::declval<U&>() = std::declval<U const&>())
. This part tests whether the specified type supports the copy assignment operation. If the type does not support it, the instantiation of thetry_assignment(U&&)
template will fail, and it will fall back to the default versiontry_assignment(...)
. This is the well-knownSFINAE, Substitution Failure Is Not An Error
.- For implementing
is_copy_constructible
,is_move_constructible
, oris_move_assignable
, a similar approach can be used by replacing the expression in this part.
- For implementing
try_assignment(...)
is an overload that can match any number and type of arguments.
1 |
|
6.1.5 has_type_member
Let’s implement has_type_member
, which checks if a given type has a member type named type
, that is, whether typename T::type
exists for type T
.
- The
primitive
version ofhas_type_member
has two type parameters, with the second parameter having a default value ofvoid
. std::void_t<T>
returnsvoid
for anyT
. For types that have a member typetype
, this specialized version iswell-formed
, so it matches this version; for types without atype
member, the deduction of the second template parameter fails, falling back to other versions. This also utilizesSFINAE
.- This example demonstrates an application of
std::void_t
. - Without
std::void_t
,has_type_member<T, typename T::type>
would not be a specialization oftemplate <typename, typename = void>
. These two forms are essentially equivalent, so even ifT::type
exists, it would still match the default version.
- This example demonstrates an application of
1 |
|
6.1.6 has_to_string
1 |
|
6.1.7 sequence
The core idea is as follows:
gen_seq<N>
: Intended to generateseq<0, 1, 2, 3, ..., N-1>
.- Use the recursive expansion
gen_seq<size_t N, size_t... S>
, whereN
represents the remaining numbers1, 2, 3, ..., N-1
, andS...
represents the already generated sequence. In each recursive step, placeN-1
into the sequence on the right. WhenN = 0
, recursion ends.
1 |
|
6.1.8 shared_ptr
1 |
|
Output:
1 | Test raw pointer |
6.2 Iterator
6.2.1 Static Loop
Inspired by base/base/constexpr_helpers.h
1 |
|
6.2.2 Iterate over std::integer_sequence
1 |
|
6.2.3 Iterate over std::tuple
1 |
|
6.3 Type Deduction
using template
: When extracting types with Traits
, we often need to add typename
to disambiguate. Therefore, using
templates can further eliminate the need for redundant typename
.
static member template
: Static member templates.
1 |
|
6.4 Static Proxy
It’s unclear if this falls strictly within the scope of metaprogramming. For more examples, refer to binary_function.h.
1 |
|
6.5 Compile-Time Branching
Sometimes, we want to write different branches of code for different types, but these branches may be incompatible across types. For example, if we want to implement addition, we can use the +
operator for int
, but for the Foo
type, we need to call the add
method. In this case, regular branching would fail, resulting in a compilation error during instantiation. We can use if constexpr
to implement compile-time branching to handle this scenario.
1 |
|
Type-specific code must be contained within if constexpr/else if constexpr
blocks. Here is an example of incorrect usage: the intention is to return immediately if the type is not arithmetic, but since left + right
is outside the static branching, an error will occur when instantiating with Foo
.
1 |
|
6.6 Implement std::bind
The following example reveals the underlying principles of std::bind
. Here’s the meaning of each helper template:
invoke
: Triggers method invocation.seq/gen_seq/gen_seq_t
: Generates an integer sequence.placeholder
: Placeholder.placeholder_num
: Counts the number of placeholders in a given parameter list.bind_return_type
: Extracts the return type of a function.select
: Extracts arguments frombindArgs
andcallArgs
. If it’s a placeholder, it extracts fromcallArgs
; otherwise, frombindArgs
.bind_t
: Encapsulates a class with an overloadedoperator()
.bind
: The interface.
Core Idea:
- First,
bind
needs to return a type, referred to asbiner_type
, which overloads theoperator()
function. - The
operator()
inbiner_type
has a parameter list that is a parameter pack,Arg...
, enabling dynamic adaptation to different bound objects.- The length of this parameter pack matches the number of placeholders.
- Use
static_assert
to constrain the parameter count to match the number of placeholders specified inbind
.
std::tuple
is used to store thebind
argument list and the parameter list ofoperator()
. Usingstd::tuple
allows easy access to the corresponding argument by index.
1 |
|
Simplified version:
1 |
|
6.7 Quick Sort
quicksort in C++ template metaprogramming
1 |
|
6.8 Conditional Members
Sometimes, we want certain specialized versions of a template class to include additional fields, while the default version does not contain these extra fields.
1 |
|
6.9 Type Guard
1 |
|
6.10 enum name
There is an key observation inspired by nameof, the enum name information cannot be generated by template itself but can come from MACRO like __PRETTY_FUNCTION__
.
Also, we cannot know the right boundary of a enum. So we can predefine a large enough valeu as the common boundary of all enum classes.
1 |
|
7 Reference
- ClickHouse
- C++雾中风景16:std::make_index_sequence, 来试一试新的黑魔法吧
- CppCon 2014: Walter E. Brown “Modern Template Metaprogramming: A Compendium, Part I”
- CppCon 2014: Walter E. Brown “Modern Template Metaprogramming: A Compendium, Part II”
- Unevaluated operands(sizeof, typeid, decltype, noexcept), 12:30
- fork_stl