Cpp Notes

modern_templmate_metaprogramming_a_compendium_part2

Modern Template Metaprogramming: A Compendium, Part II - Walter E. Brown

Using inheritance + specialization together

// Example 1: given a type, is it a void type?

// primary template for non-void types:
template <class T>
struct is_void : false_type {};

// specializations recognize each of the four void types:
template <>
struct is_void<void> : true_type {};
template <>
struct is_voids<void const> : true_type {};
//...

// Example 2: given two types, are they one and the same?
// primary template for distinct types:
template <class T, class U>
struct is_same : false_type {};
// partial specialization recognizes identical types:
template <class T>
struct is_same<T, T> : true_type {};

Example 1: is_void Metafunction

  • Purpose: Determines whether a given type is a void type.
  • Implementation:
    • The primary template assumes the type is not void, inheriting from false_type.
    • Specializations for void and its cv-qualified variants (void const in the provided example, along with void volatile and void const volatile not shown) inherit from true_type, indicating the type is indeed void.
  • Notes: The example mentions the existence of four void types to account for cv-qualifiers (const and volatile). This approach utilizes inheritance from integral_constant (via true_type and false_type) to provide a compile-time boolean value as the metafunction result.

Example 2: is_same Metafunction

  • Purpose: Checks if two provided types are identical.
  • Implementation:
    • The primary template assumes the types are distinct, inheriting from false_type.
    • A partial specialization for when both template parameters are the same type (is_same<T, T>) inherits from true_type, indicating the types are identical.
  • Notes: This metafunction is a prime example of template metaprogramming's power to compare types directly at compile time, serving as a fundamental tool for type-based decision-making in generic programming.

Key Concepts Illustrated

  • Inheritance and Specialization: Both examples leverage inheritance from a base template (integral_constant) and use template specialization to customize behavior based on type traits. This technique is essential in C++ template metaprogramming for creating compile-time logic.
  • Simplicity and Utility: These metafunctions underscore the principle that utility in programming, especially in template metaprogramming, does not necessarily come from complexity. Even straightforward constructs can provide significant value by enabling type-safe generic programming and compile-time decision-making.
  • Standard Library Integration: Both is_void and is_same are part of the C++ Standard Library's type traits, highlighting the library's comprehensive support for compile-time type inspection and manipulation. These traits are crucial for developing robust, type-aware templates that adapt to various type conditions.

Forwarding/delegating to other metafunctions

// Example : given a type, is it a void type

template <class T>
using is_void = is_same<remove_cv_t<T>, void>;

// Where remove_cv and remove_cv_t are simply
template <class T>
using remove_cv = remove_volatile<remove_const_t<T>>;

template <class T>
using remove_cv_t = typename remove_cv<T>::type;
  • The revised example of the is_void metafunction demonstrates a streamlined approach to type traits in C++ template metaprogramming, leveraging the composition of metafunctions and type aliases for clear and concise type manipulations.
  • This approach showcases the power of abstraction and reuse in metaprogramming by building upon existing metafunctions. Here's an in-depth explanation:

Simplified is_void Metafunction

  • Concept: The is_void metafunction now indirectly checks if a type is void by first removing any const and volatile qualifiers from the type and then comparing the result with void using is_same.
  • Implementation Details:
    • remove_cv_t<T>: A type alias that removes both const and volatile qualifiers from type T. It's defined through a composition of remove_const_t and remove_volatile metafunctions.
    • is_void Definition: Utilizes is_same to compare the type T (after removing const and volatile qualifiers) with void. If the types match, is_same inherits from true_type; otherwise, it inherits from false_type.
    • Usage of Aliases: The example employs remove_cv_t to simplify the syntax and enhance readability, avoiding the verbose and more cumbersome syntax of nested template instantiations.

Key Points

  • Metafunction Composition: The example illustrates how metafunctions can be composed to perform complex type queries through simple, readable expressions. This compositionality is a cornerstone of effective template metaprogramming, enabling the creation of expressive and modular type utilities.
  • Type Aliases for Clarity: The use of type aliases (remove_cv_t, is_void) simplifies the implementation and usage of metafunctions, making the code more accessible and reducing boilerplate. This approach enhances the expressiveness of template metaprogramming, allowing for more intuitive type manipulations.
  • C++14 Alias Templates: The example highlights the utility of C++14's alias templates (_t suffixes) in metafunction calls, streamlining the syntax for obtaining the result of a metafunction. This feature eliminates the need for the typename keyword and ::type syntax, further simplifying template metaprogramming.

Using a parameter pack in a metafunction

// Example: generalize is_same into is_one_of:
// primary template: is T the same as any of the types POtoN ... ?
template <class T, class... POtoN>
struct is_one_of; // declare the interface only, no definition

// base #1: specialization recognizes empty list of types:
template <class T>
struct is_one_of<T> : false_type {};

// base #2: specialization recognizes match at head of list of types:
template <class T, class... P1toN>
struct is_one_of<T, T, P1toN...> : true_type {};

// specialization recognizes mismatch at head of list of types:
template <class T, class PO, class... P1toN>
struct is_one_of<T, PO, P1toN...> : is_one_of<T, P1toN...> {};
  • Objective: To determine if a specific type T matches any type within a given list of types (POtoN...).

Detailed Breakdown

  • Primary Template Declaration:

    • The primary template for is_one_of is declared without a definition, indicating it's designed to be specialized. This approach is common in template metaprogramming when dealing with variadic templates, as it allows for more precise control over template instantiation based on specific conditions.
  • Base Case #1 - Empty Type List:

    • A specialized version for when the type list (POtoN...) is empty. Since there are no types to compare against T, it inherits from false_type, indicating T is not found among the types (because there are none).
  • Base Case #2 - Match at Head of List:

    • When the first type in the list matches T, this specialization is selected, inheriting from true_type. This case demonstrates template specialization's power to perform compile-time pattern matching, effectively checking if T matches the first type in the list.
  • Recursive Case - Mismatch at Head:

    • If the first type (PO) does not match T, this specialization recurses with the rest of the type list (P1toN...), effectively discarding the first type and checking the remainder. This recursive pattern mimics functional programming paradigms, akin to list processing in languages like Lisp, where operations are performed on the head of a list, and recursion handles the tail.

Re-revisiting is_void

// Example: given a type, is it a void type?
template <class T>
using is_void =
    is_one_of<T, void, void const, void volatile, void const volatile>;
  • if you happen to be a library vendor, of course you have to make an implementation decision, but now you can make an engineering decision.
  • What's going to involve the fewest template instantiations or the least amount of compile time?
  • Whatever your criteria are. But the point is we tend to have many more choices, even when metaprogramming, than we realize.

Unevaluated operands

// Recall that operands of sizeof, typeid, decltype, and
// noexcept are never evaluated, not even at compile time:
// - Implies that no code is generated (in these contexts)
//   for such operand expressions, and ...
// - Implies that we need only a declaration, not a definition,
//   to use a (function's or object's) name in these contexts.
//
//  An unevaluated function call (e.g., to foo) can usefully map
//  one type to another:
decltype(foo(declval<T>()))
// gives foo's return type, were it called with a T rvalue
// The unevaluated call declval<T>() is declared to give
// an rvalue result of type T. (declval<T&>() gives Ivalue.)

Key Points on Unevaluated Contexts in C++

  • Unevaluated Operands: In C++11 and later versions, certain language constructs such as sizeof, typeid, decltype, and noexcept do not evaluate their operands. This means they can inspect types, functions, and expressions without triggering any computation or requiring executable code.

    • Unevaluated. It means all we're going to do is name them. We're going to look at them. We're going to inspect them. We're not going to use them, because that would be an evaluation.
  • No Code Generation: Since these operands are not evaluated, using them does not generate any executable code. The implications are twofold: there is no runtime cost, and compile-time efficiency is maintained since only type information is processed.

  • Declaration Sufficiency: For expressions within these contexts, only declarations are required, not definitions. This allows developers to refer to functions or objects by name for type inspection or manipulation purposes without needing a complete definition at compile time.

  • Using decltype and declval:

    • decltype is used to deduce the type of an expression without evaluating it. When applied to a function call, decltype can deduce the return type based on the function's signature, even if the function is overloaded.
    • declval is a utility that provides a way to "pretend" to have a value of a given type without actually creating it. This is particularly useful for deducing types in contexts where creating an instance of a type might be impractical or impossible (e.g., abstract classes).
    • declval is only usable in unevaluated contexts because it does not have a definition, ensuring it cannot be mistakenly used in a context that requires an actual instance.

Practical Application

  • This approach is invaluable in template metaprogramming for creating type traits, compile-time condition checks, and other metaprogramming utilities that require type inspection or manipulation without the overhead of execution.
  • For example, determining the return type of a hypothetical function call without actually calling the function simplifies the creation of type-dependent logic, enabling more expressive and flexible template code.

Example: testing for copy-assignability (incomplete yet, hold on)

template <class T>
struct is_copy_assignable {
 private:
  template <class U, class = decltype(declval<U&>() = declval<U const&>())>
  static true_type try_assignment(U&&);   // SFINAE may apply!
  static false_type try_assignment(...);  // catch-all overload

 public:
  using type = decltype(try_assignment(declval<T>()));
};

// Note: This is incomplete - still need to ensure that the assignment's
// result type is T&

Key Components of the Example

  • Use of decltype and declval:

    • decltype is employed to deduce the type of an expression, specifically the return type of a function or operation, without evaluating the expression itself.
    • declval is utilized to simulate an instance of a type for decltype to inspect, especially useful when dealing with types that cannot be instantiated (e.g., abstract classes, types without default constructors).
  • SFINAE to Determine Copy-Assignability:

    • The technique employs a clever use of SFINAE by defining two overloads of a try_assignment function.
    • The first overload attempts to mimic the copy assignment operation using decltype(declval<U&>() = declval<U const&>()) as its template parameter's default argument. If the type U is not copy-assignable, substitution into this expression fails, invoking SFINAE.
      • declval is a utility function from the header that allows you to create an rvalue of any type without needing to construct an actual object.
      • U& and U const& represent an lvalue reference and a const lvalue reference to an object of type U, respectively.
      • U& = U const& is an attempt to perform copy assignment from a const object to a non-const object. If U is copy-assignable, this operation is valid, and the result of the decltype expression is U&. If U is not copy-assignable, this operation would fail.
    • The second overload is a catch-all that matches any type but is considered the worst match for overload resolution, ensuring it is only selected if the first overload is not viable due to SFINAE.
      • If the copy assignment expression is ill-formed for the given type U (for example, if U lacks a copy assignment operator or if it's deleted/explicitly private), SFINAE kicks in. The compiler discards the first overload as a candidate, leaving the catch-all overload with ... as the only viable option, leading to a result of false_type.
  • Implications and Utility:

    • This technique underscores the power of unevaluated contexts in C++ for metaprogramming, allowing developers to introspect type capabilities and characteristics at compile time, significantly enhancing template-based logic and type checks.
    • It also highlights the evolution of C++ towards more expressive and powerful metaprogramming capabilities, particularly with the introduction of features in C++11 and later that support these advanced techniques.

Recap: Older technique to create "unevaluated context"

// Before C++11 introduced decltype, sizeof was (ab)used
// to provide an unevaluated metaprogramming context:
// Useful to keep this in mind when reading earlier
// (C++98/C++03) template metaprogramming code.
// In detail:
// 1. Overloads' return types were crafted to have distinct sizes,
//    e.g.,
typedef char (&yes)[1];
typedef char (&no)[2];

// 2. As before, call the overloaded function in an unevaluated
//    context:
sizeof(try_assignment(...));

// 3. To determine which overload was chosen:
typedef bool_constant<sizeof(...) == sizeof(yes)> type;
  • The recap highlights how developers utilized sizeof in C++98/C++03 to create an unevaluated context for template metaprogramming before decltype was introduced in C++11.
  • This technique, while ingenious, underscores the limitations and the lengths programmers went to in order to achieve compile-time type checks and metaprogramming tasks. Let's delve into the details and corrections of this approach:

Technique Overview

Apologies for the confusion. Let's break it down step by step:

  1. Define Typedefs: First, typedefs yes and no are defined. These typedefs are used to create references to arrays of different sizes. For example:

    typedef char (&yes)[1];
    typedef char (&no)[2];
  2. Overload Functions: Next, overloaded functions are defined with different return types. These functions serve as a way to create an unevaluated context for type deduction. For example:

    yes try_assignment(...); // Returns a reference to an array of size 1
    no try_assignment(...);  // Returns a reference to an array of size 2
  3. Invoke in Unevaluated Context: These overloaded functions are then called using sizeof. The result of sizeof provides a compile-time constant value that can be used for conditional compilation. For example:

    sizeof(try_assignment(...)); // Produces a compile-time constant (size)
  4. Comparison for Type Selection: The result of sizeof is compared against the size of yes (or any known size) to determine which overload was chosen. For example:

    sizeof(try_assignment(...)) == sizeof(yes)

Correction and Clarification

  • Correctness and Completeness: The technique is correctly identified as a workaround for the limitations of pre-C++11 metaprogramming but is noted as incomplete for fully assessing copy-assignability. Specifically, it doesn't check the return type of the assignment operator, which should conventionally be a reference to the assigned object's type for proper assignment semantics.

  • Modern Alternatives: With the introduction of decltype in C++11, much of the need for such convoluted techniques has been obviated. decltype allows direct inspection of types and expressions in an unevaluated context, simplifying the implementation of type traits and other compile-time checks.

Proposed new type trait void_t

// Near-trivial to specify:
template <class...>
using void_t = void;

// May need a workaround for older compiler), pending resolution (at next
// meeting?) of CWG issue 1558 clarifying "treatment of unused arguments
// in an alias template specialization":

template <class...>
struct voider {
  using type = void;  // helper to step around CWG 1558
};

template <class... TOtoN>
using void_t = typename voider<TOtoN...>::type;
// In either case, how does another spelling of void help us ?
  • The discussion introduces void_t, a utility in C++ metaprogramming, to facilitate SFINAE (Substitution Failure Is Not An Error) techniques by mapping any number of type arguments to void. This seemingly trivial type alias, proposed to enhance template metaprogramming, serves as a powerful tool for detecting type properties and features without invoking code execution. Let's break down the concept, utility, and application of void_t:

Concept of void_t

  • Definition: void_t is defined as a template alias that accepts an arbitrary number of type arguments and always yields void as its type. It's a simple yet profound concept that taps into the core of C++'s template metaprogramming capabilities.

Utility of void_t

  • Simplifying SFINAE: void_t simplifies the creation of SFINAE-friendly type traits. It allows template specializations to be conditionally enabled based on the well-formedness of types or expressions, significantly enhancing the expressiveness and power of template metaprogramming.

  • Detecting Type Properties: By using void_t, programmers can easily check for the presence or absence of certain properties, methods, or member types within a given type, streamlining the process of introspecting type characteristics at compile time.

Application Example: Detecting a Type Member

  • Detecting T::type Presence: The example demonstrates how void_t can be employed to detect whether a given type T has a nested type member named type. This is achieved through a clever use of template specialization and void_t.
// Utility of void
// Acts as a metafunction call that maps any well-formed
// type(s) into the (predictable!) type void:

// Initially an implementation detail while proposing SFINAE-
// friendly versions of common_type and iterator_traits.

// Example: detect the presence/absence of a type member
// named (for example) T::type:

// 1. primary template:
template <class, class = void>
struct has_type_member : false_type {};

// 2. partial specialization:
template <class T>
struct has_type_member<T, void_t<typename T::type>> : true_type {};

// In detail
// Called via has_type_member<T>::value or equivalent.

// When type T does have a type member named type:
template <class T>
struct has_type_member<T, void_t<typename T::type>> : true_type {};
// This specialization is well-formed (despite a funny spelling
// of the second argument, void) and will be selected (as the
// better viable candidate).

// When type T does not have a type member named type:
// T::type is ill-formed, so the specialization 2 is nonviable
// (SFINAE!); as the only viable candidate, this primary template
// will be selected.
template <class, class = void>  // default argument is essential
struct has_type_member : false_type {};

Implementation Breakdown

  • Primary Template: The primary template assumes the absence of the T::type member, defaulting to false_type.

  • Partial Specialization: A specialization uses void_t to check for T::type's presence. If T::type is well-formed, the specialization inherits from true_type, indicating the presence of the type member. Otherwise, SFINAE causes this specialization to be discarded, and the primary template is selected.

  • SFINAE-Friendly: This technique leverages void_t to create a SFINAE-friendly environment. It ensures that only well-formed types influence the specialization choice, making it an elegant solution for compile-time type inspection.

Evolution and Impact

  • Historical Context: Before decltype and void_t, programmers resorted to using sizeof in complex ways to achieve similar results. The introduction of void_t and similar constructs marks a significant evolution in the language, offering more straightforward and powerful metaprogramming capabilities.

  • Standardization Efforts: Given its utility, there have been proposals to include void_t in the C++ Standard Library, recognizing its value in simplifying and enabling more robust template metaprogramming techniques.

Conclusion

void_t exemplifies the depth and flexibility of C++ template metaprogramming, providing a minimalist yet powerful tool for type introspection and SFINAE-based techniques. Its introduction and potential standardization reflect ongoing efforts to enhance the language's metaprogramming facilities, making C++ an even more expressive and powerful tool for developers.

Revisiting is_copy_assignable

// helper alias for the result type of a valid copy assignment:
template <class T>
using copy_assignment_t = decltype(declval<T&>() = declval<T const&()>);

// primary template handles all non-copy-assignable types:
template <class T, class = void>  // default argument is essential
struct is_copy_assignable : false_type {};

// specialization recognizes and validates only copy-assignable types:
template <class T>
struct is_copy_assignable<T, void_t<copy_assignment_t<T>>>
    : is_same<copy_assignment_t<T>, T&> {};

// done. Want is_move_assignable?
// Change T const& to T&&.
  • The discussion on is_copy_assignable type trait improvement introduces a more refined and accurate approach to determine if a type is copy-assignable using modern C++ techniques, specifically leveraging SFINAE, decltype, and the utility alias void_t.
  • This revised method addresses limitations in earlier implementations by ensuring that not only the presence of a copy assignment operator is checked but also its return type matches the expected T& (reference to the type).

Improved is_copy_assignable Implementation

  • Helper Alias (copy_assignment_t): This alias uses decltype to deduce the return type of a copy assignment expression. It simplifies repeated expressions and makes the main template cleaner and more readable.

  • Primary Template: Defaults to false_type, covering all types that don't meet the criteria for being copy-assignable. This is a safe starting assumption that will be overridden only if a type meets the specific requirements.

  • Specialization with void_t: This is where the magic happens. The specialization checks if copy_assignment_t<T> is well-formed (indicating the type T has a valid copy assignment operator) and additionally checks if the return type of this assignment operation is T&. This is done by further specializing with is_same<copy_assignment_t<T>, T&>, which ensures that the type adheres to the conventional copy assignment operator signature.

Significance

  • Accuracy and Precision: This approach not only checks for the existence of a copy assignment operator but also validates its conformity to expected C++ semantics (i.e., returning a reference to the assigned object). This increases the precision of the type trait, making it more reliable for template metaprogramming.

  • SFINAE-Friendly: The use of void_t to wrap the copy_assignment_t check ensures that the trait remains SFINAE-friendly. If a type T does not have a copy-assignable operator or if the operator does not return T&, the code remains well-formed, and false_type is derived.

  • Extensibility to is_move_assignable: By illustrating the switch from T const& to T&& for move assignment checks, the example highlights the trait's adaptability to check for move assignability, showcasing the versatility of the approach.

Personal Notes: Why primary template default class = void is essential?

Aha moment: what matters is when copy_assignment_t<T> is NOT SFINAE out

But first, you need to know how partial template specialization works:

  • The argument list cannot be identical to the non-specialized argument list (it must specialize something):

  • Partial template specializations are not found by name lookup. Only if the primary template is found by name lookup, its partial specializations are considered.

  • When a class or variable(since C++14) template is instantiated, and there are partial specializations available, the compiler has to decide if the primary template is going to be used or one of its partial specializations.

      1. If only one specialization matches the template arguments, that specialization is used
      1. If more than one specialization matches, partial order rules are used to determine which specialization is more specialized. The most specialized specialization is used, if it is unique (if it is not unique, the program cannot be compiled)
      1. If no specializations match, the primary template is used
  • Modified from reply in this thread

#include <iomanip>
#include <iostream>
#include <string>

template <typename, typename = void>
struct has_size_type : std::false_type {};

template <typename T>
struct has_size_type<T, std::void_t<typename T::size_type>> : std::true_type {};

namespace bad_generic {

using any_non_void_type = int;

template <typename, typename = any_non_void_type>
struct has_size_type : std::false_type {};

template <typename T>
struct has_size_type<T, std::void_t<typename T::size_type>> : std::true_type {};
}  // namespace bad_generic

namespace bad_specialization {
template <typename, typename = void>
struct has_size_type : std::false_type {};

template <typename T>
struct has_size_type<T, typename T::size_type> : std::true_type {};
}  // namespace bad_specialization

int main() {
  constexpr bool a = has_size_type<std::string>::value;
  // 1. name look up finds the primary template
  //    has_size_type<typename, typename>
  //    with template args: has_size_type<std::string, void>
  // 2. look for specializations that match has_size_type<std::string, void>
  //    ... not found
  // 3. look for partial specializations that match
  //    has_size_type<std::string, void>
  // 4. found partial specialization
  //    has_size_type<T, std::void_t<typename T::size_type>>
  //    matches has_size_type<std::string, void>
  // 5. there are no other partial specializations; select this specialization
  static_assert(a,
                "error: std::string must have a nested type/type alias size_t");

  constexpr bool bg = bad_generic::has_size_type<std::string>::value;
  // 1. name look up finds the primary template
  //    bad_generic::has_size_type<typename, typename>
  //    with template args: bad_generic::has_size_type<std::string, int>
  // 2. look for specializations that match
  //    bad_generic::has_size_type<std::string, int> ... not found
  // 3. look for partial specializations that match
  //    bad_generic::has_size_type<std::string, int> ... not found
  // 4. there are no other partial specializations; select primary generic
  //    template
  static_assert(
      !bg,
      "error: bg must be false (primary template must have been selected)");

  constexpr bool bs = bad_specialization::has_size_type<std::string>::value;
  // 1. name look up finds the primary template finds
  //    bad_specialization::has_size_type<typename,typename>
  //    with template args: bad_specialization::has_size_type<std::string, void>
  // 2. look for specializations that match
  //    bad_specialization::has_size_type<std::string, void> ... not found
  // 3. look for partial specializations that match
  //    bad_specialization::has_size_type<std::string, void>
  // 4. see partial specialization
  //    bad_specialization::has_size_type<T, typename T::size_type>
  //    but it's not a match because std::string::size_type != void
  //    so ignore this specialization
  // 5. there are no other partial specializations; select primary generic
  //    template
  static_assert(
      !bs, "error: b must be false (primary template must have been selected)");
}

More search result: how default template arguments interact with template specializations

  • In the context of template specializations and default template arguments, it's important to differentiate between the primary template and its specializations.
  • The primary template provides a generic definition that may use default template arguments, and it may be specialized further for specific cases.
template<class T = void> struct foo { ... }; // #1
template<> struct foo<void> { ... }; // #2
  • In this example, foo is a class template with a default template argument of void.

  • However, this default argument is just part of the template declaration, not the definition.

  • The primary template is provided in #1, and it serves as the generic definition for foo.

  • Then, in #2, there's a specialization specifically for foo<void>.

  • This pattern clarifies the role of default template arguments in the primary template and how specializations can be made based on these defaults. It helps to separate the declaration with default arguments from the main definition and its specializations.

  • Similarly, in the context of has_member_function, the default template argument void is used implicitly in the specialization. If void_t<copy_assignment_t<T>> is well-formed, the specialization has_member_function<T, void> is instantiated. Otherwise, due to SFINAE (Substitution Failure Is Not An Error), that specialization doesn't exist, and the primary template serves as the fallback.

Summary of techniques and tools

  • Metafunction member types, static const data members, and constexpr member functions to express intermediate and final metafunction results.
  • Metafunction calls (possibly recursive), inheritance, and aliasing to factor commonalities.
  • Template specializations (complete and partial) for metafunction argument pattern-matching.
  • SFINAE to direct overload resolution.
  • Unevaluated operands, such as function calls to map types.
  • Parameter packs as lists (usually of types).
  • Recent and classical std::metafunctions in <type_traits>, , <numeric_limits>, etc.
  • void t, and more!

A final thought re template metaprogramming

  • "Although we're professionals now, we all started out as humble students .... Back then, everything was new, and we had no real way of knowing whether what we were looking at was wizardry or WTF."
  • For those of us in the business, everybody's got a war story. This is a source of war stories.... And part of us, as we go on our journey from what were the classical terms, apprentice to juryman to master, we had to learn how to do this assessment. Because when you're an apprentice, you don't know anything. And when you're a master, you're supposed to know just about everything. And at least to know what you don't know. Almost everybody, like me, we fall in between somewhere those extremes. So yeah, temple of metaprogramming. Is it wizardry or is it a WTF? Each of us has to decide. I've made my decision. I wish you well as you make yours. Thank you all very much. Thank you.