C++ template metatemplate (heterogeneous dictionary and policy template) – – – Part 2

Table of Contents

1. Representation of keys

1.1 Numerical conflict problem

1.2 Identifier management

2.3 Identifier management solution

2.4 How to pass string type as template parameter?

2.5 String literals

2.6 The trouble with strings as VarTypeDict keys

2.7 Use class (or structure) name as key

2. Brief performance analysis of VarTypeDict

3. Use std::tuple as cache

Summarize

1. Representation of keys

Code example:

#include <iostream>

constexpr int A = 0;
constexpr int B = 1;
constexpr int Weight = 2;

template <int A, int B, int Weight>
struct VarTypeDict {
    // You can use A, B, and Weight as compile-time constants here
};

int main() {
    VarTypeDict<A, B, Weight> myVarTypeDict;

    // You can use myVarTypeDict to operate here

    return 0;
}

1.1 Numerical Conflict Problem

Use the enum class in C++ to create separate types for A, B, and Weight to avoid numeric conflicts.

Sample code:

#include <iostream>

enum class A : int { Value = 0 };
enum class B : int { Value = 1 };
enum class Weight : int { Value = 2 };

template <typename T>
struct VarTypeDict {
    // You can use T::Value as a compile-time constant here
};

int main() {
    VarTypeDict<A> varA;
    VarTypeDict<B> varB;
    VarTypeDict<Weight> varWeight;

    // You can use varA, varB, varWeight to operate here

    return 0;
}

In the example, we use the enum class `enum class` to represent A, B and Weight respectively, and give them independent scopes. Each enumeration class has a member named Value that represents a specific value. In this way, when calling Set or Get in a function, by passing in the type of the enumeration class, the compiler can clearly distinguish which member variable is to be set or read.

1.2 Identifier Management

To implement a mechanism for managing identifiers, this can be accomplished by defining a centrally managed module or class. This module or class can be responsible for allocating, recording and managing various identifiers, while providing interfaces for other modules or classes to register and query assigned identifiers.

Code example:

#include <iostream>
#include <map>
#include <string>

// Enumeration class is used to represent different identifiers
enum class Identifier {
    ID_A,
    ID_B,
    ID_C,
    // ... other identifiers
};

//Identifier management class
class IdentifierManager {
public:
    //Registration identifier and its meaning
    static void registerIdentifier(Identifier id, const std::string & amp; meaning) {
        identifierMap()[id] = meaning;
    }

    // Query the meaning of the identifier
    static std::string getIdentifierMeaning(Identifier id) {
        auto it = identifierMap().find(id);
        if (it != identifierMap().end()) {
            return it->second;
        } else {
            return "Undefined";
        }
    }

private:
    // Get identifier mapping
    static std::map<Identifier, std::string> & amp; identifierMap() {
        static std::map<Identifier, std::string> map;
        return map;
    }
};

int main() {
    //Registration identifier and its meaning
    IdentifierManager::registerIdentifier(Identifier::ID_A, "This is A");
    IdentifierManager::registerIdentifier(Identifier::ID_B, "This is B");

    // Query the meaning of the identifier
    std::cout << "ID_A means: " << IdentifierManager::getIdentifierMeaning(Identifier::ID_A) << std::endl;
    std::cout << "ID_B means: " << IdentifierManager::getIdentifierMeaning(Identifier::ID_B) << std::endl;
    std::cout << "ID_C means: " << IdentifierManager::getIdentifierMeaning(Identifier::ID_C) << std::endl;

    return 0;
}

In this example, we define an enumeration class `Identifier` to represent different identifiers, and then implement a class named `IdentifierManager` to manage these identifiers. The `IdentifierManager` class provides interfaces for registering identifiers and querying identifier meanings, as well as an internal identifier mapping table as a static member function for recording identifiers and their corresponding meanings.

In the `main` function, we registered the identifiers A and B through the `IdentifierManager` class and assigned meanings to them respectively, and then performed a query and printout of the meanings. In large projects, more complex mechanisms may be needed to manage identifiers, such as allocation scope, collaboration specifications, version control, etc.

2.3 Identifier Management Solution

example:

1. Automatically generate identifiers: Consider writing tools or scripts to automatically generate mappings of identifiers and their meanings, which can reduce the workload of manual management. For example, you can create a configuration file that lists all the identifiers and their meanings, and then write a tool to read the configuration file and generate the corresponding mapping code.

2. Use a symbol table or database: Store identifiers and their meanings in a centralized symbol table or database. Other modules can obtain the meaning of identifiers by querying these symbol tables or databases without maintaining their own mapping tables.

3. Adopt semantic naming conventions for identifiers: By adopting semantic naming conventions, the need for manual management of identifiers can be minimized. Such a naming convention can make the meaning of identifiers more intuitive and clear, and reduce the management work of mapping.

4. Version control and code review: When multiple people collaborate on development, strict version control specifications and code review mechanisms can help the team better understand and coordinate the use of identifiers in each part, and discover potential conflicts and problems in a timely manner. .

5. Regularly clean up unused identifiers: Regularly clean up unused identifiers in the system to avoid a large number of useless entries in the mapping table and reduce management costs.

The above suggestions are intended to reduce the maintenance costs of identifier management and improve code maintainability and development efficiency.

2.4 How to pass string type as template parameter?

In C++, the `std::string` type in the standard library itself cannot be passed directly as a template parameter because the acceptance range of template parameters is limited. There are two types of C++ template parameters: class template parameters and non-type template parameters. For non-type template parameters, the acceptable types mainly include integers, enumerations, pointers, references, etc., but do not include class types (class types). Therefore, class types such as `std::string` cannot be directly used as non-type template parameters. Type template parameters.

However, C++11 introduced the two features of template aliases (Template Aliases) and variable-length parameter templates (Variadic Templates), allowing us to indirectly implement non-type parameters such as passing strings through some indirect methods:

1. Template aliases: By using template aliases, you can define an alias for a specific string type and then pass the alias as a template parameter.

Example:

 template <typename T, T val>
   struct MyStruct {};

   using MyString = std::integral_constant<const char*, "hello">;
   MyStruct<MyString, MyString::value> obj;

2. Variable-length parameter template: The variable-length parameter template can be used to convert strings and then pass them as template parameters.

Example:

 template <char... Args>
   struct StringHolder {
       static constexpr const char value[] = {Args..., '\0'};
   };

   StringHolder<'h', 'e', 'l', 'l', 'o'> obj;

Or, if the scenario allows, you can consider using template metaprogramming technology, combined with specialization, overloading and other means, to achieve the purpose of passing strings (such as qualified character arrays) as template parameters. However, this approach may introduce complexities and limitations.

2.5 string literal

The C++11 standard introduces non-type template parameter support for string literals, allowing string literals to be passed as template parameters. In this case, we can use different string literals to generate different instances during the template instantiation process, thereby templating the string type.

1. Declare strings by reference:

 template <const char ( & amp;str)[N]>
   struct MyStruct {
       // ...
   };

   const char hello[] = "Hello";
   MyStruct<hello> obj1;

   const char cpp[] = "C + + ";
   MyStruct<cpp> obj2;

In this way, strings of different lengths are treated as different types, so that strings of different lengths can be distinguished.

2. Declare strings as values:

 template <const char* str>
   struct MyStruct {
       // ...
   };

   MyStruct<"Hello"> obj1;
   MyStruct<"C + + "> obj2;

In this way, string literals are cast to the corresponding pointer type, so regardless of the length of the string, its type is cast to `const char*`, so strings of the same length are treated as the same type.

It is worth noting that special care needs to be taken when using string literals as non-type template parameters, because the address of the string literal may be different in different compilation units, which may cause some potential problems. In actual use, it is recommended to carefully consider the applicability of this method and be prepared for potential problems.

Hope this information answers your questions. If you have more questions about this or other questions, please feel free to ask.

2.6 The troubles with strings as VarTypeDict keys

Key issues include:

1. Memory overhead: Using strings as keys may cause additional memory overhead because the string itself needs to be stored, and the string is discontinuous in memory, so additional fragmentation problems may be introduced.

2. Comparison overhead: Comparison between strings usually requires character-by-character comparison, which results in high comparison overhead, especially in the case of large amounts of data.

3. Hash calculation overhead: If a string needs to be hashed to achieve fast search, then the hash calculation of the string will also bring a certain overhead.

4. Complexity: Using strings as keys may introduce additional complexity, including string copying, comparison, hash calculation and other operations, as well as considerations of conflict handling.

Based on the above issues, in actual applications, if performance requirements are high, you may be inclined to use integer types as keys, because comparison and hash calculations of integer types are usually more efficient than strings.

Of course, not all cases will choose integers as keys, especially when it needs to be descriptive and easy to understand. Using strings as keys is a natural choice. In this case, you may need to consider factors such as performance, readability, and maintenance costs to select the most suitable data structure or algorithm. .

2.7 Use the name of the class (or structure) as the key

In fact, using class (or structure) names as keys is a very natural and elegant approach. Since the class name is determined during compilation and is unique, it can be used as a key in the compiler.

This has several advantages:

1. Uniqueness and certainty: Each class name is unique and certain in the program, ensuring the uniqueness of the key.

2. Easy to compare: Comparisons between class names are direct and efficient because they are determined at compile time and do not require additional runtime calculations.

3. Type safety: The class name as a key has strong type safety, because the type of the class name is statically determined, and there will be no problems caused by type differences.

4. Readability: When using code, the class name as a key can intuitively represent the type it refers to, improving the readability and maintainability of the code.

In C++, template metaprogramming can be used to map class names as keys, such as using template metaprogramming techniques or type mapping tables to map class names to corresponding values or other information. . This method can avoid the problems caused by using strings as keys to a certain extent, and can better utilize the static type system of C++.

Code example:

#include <iostream>
#include <string>

// main template
template <typename T>
struct TypeToIntMap {
    // Generic template, does not define any content
};

//Specialize the template to map class names to integers
template <typename T>
struct TypeToIntMap {
    static const int value;
};

template <typename T>
const int TypeToIntMap<T>::value = 0;

// Specialize here to map class names to specific integers
template <>
struct TypeToIntMap<int> {
    static const int value;
};

const int TypeToIntMap<int>::value = 1;

template <>
struct TypeToIntMap<double> {
    static const int value;
};

const int TypeToIntMap<double>::value = 2;

// test
int main() {
    //Use TypeToIntMap
    std::cout << "int is mapped to: " << TypeToIntMap<int>::value << std::endl;
    std::cout << "double is mapped to: " << TypeToIntMap<double>::value << std::endl;

    return 0;
}

In the example, we define a TypeToIntMap template to map different types to different integers. For each type that needs to be mapped, we use a template specialization to assign it a unique integer value.

In the main function, we show how to use TypeToIntMap to get the integer values corresponding to different class names. When you compile and run this code, you will see that the class name is successfully mapped to the corresponding integer value.

2. Brief performance analysis of VarTypeDict

Content Analysis:

The VarTypeDict class achieves superior performance and efficiency by mapping keys to values at compile time, which has obvious advantages over std::map or similar data structures constructed at runtime.

Key mapping completed at compile time has great benefits for performance and resource utilization.

1. Compilation-time calculation: VarTypeDict completes the mapping of keys to values at compile-time, thus avoiding comparison and calculation at runtime and improving program execution efficiency.

2. Optimization opportunities: Calculations performed at compile time can usually be better optimized, and even intermediate storage can be completely eliminated under certain circumstances, greatly reducing memory usage.

3. Incomparable to run-time equivalents: The performance advantages brought by compile-time calculations are incomparable to run-time equivalents, which reflects the power of calculations at compile time.

In general, the VarTypeDict class implements efficient key-value mapping by fully utilizing the characteristics of compile-time calculations, and its performance is far superior to the standard library container constructed at runtime. This idea of using compile-time calculations is part of the “do as much work as possible at compile time” advocated in modern C++.

3. Use std::tuple as cache

std::shared_ptr<void> m_tuple{sizeof...(TTypes)};
std::tuple<<TType...> m_tuple;

First of all, it needs to be pointed out that std::shared_ptr is a smart pointer, which can point to any type of object, while std::tuple is a tuple type, which contains a set of values of different types.

Using std::tuple may introduce more complex update logic, especially when values need to be updated in tuples, as the value types in tuples may be different, which may cause updates Logic complexity increases.

Due to different types, the assignment operation requires copying or moving each element in the array one by one, which will cause a significant loss in performance.

You can consider using some optimization methods to minimize this overhead, such as using move semantics to reduce the cost of data movement, or using the idea of lazy evaluation to delay the actual data copy operation.

Summary

The implementation of heterogeneous dictionaries involves a lot of content and will be explained separately in the next issue! ! !