173. Generic Sensor Pair

In embedded telemetry systems, sensor data is rarely transmitted alone. It is commonly paired with metadata such as a timestamp, a status flag, or an identifier. Writing separate structs for every possible combination (for example, FloatWithTimestamp, IntWithStatus) leads to unnecessary code duplication and poor scalability.

Your task is to implement a class template named SensorPair that generically stores a sensor value together with its associated metadata.

The class template must accept two template parameters:

  • typename T_Value — the type of the sensor measurement (for example, float, int)
  • typename T_Meta — the type of the metadata (for example, int, char)

The goal is to practice multi-parameter templates, type-generic design, and compile-time type safety, which are commonly used in embedded C++ firmware codebases.

Requirements:

The SensorPair class template must:

  1. Declare two public data members:
    • value of type T_Value
    • metadata of type T_Meta
  2. Provide a constructor that initializes both members using a member initializer list.
  3. Provide a public member function print() that prints the pair in the exact format:

    Pair: <value> | <metadata>
    

Program Flow:

  1. Read an integer N representing the number of test cases.
  2. Repeat N times:
    • Read a string type_code
    • Based on type_code, read the appropriate input values:
      • "f-i" → read a float value and an int metadata
      • "i-c" → read an int value and a single character metadata
      • "i-i" → read an int value and an int metadata
    • Instantiate the correct specialization of SensorPair
    • Call print() on the created object

Input Format:

  • First line: Integer N (1 ≤ N ≤ 20)
  • Next N lines:
    • A string type_code
    • Followed by exactly two values matching the specified types
  • All input is provided via standard input (stdin)
  • For "i-c" cases, the metadata is always a single character, not a string

Output Format:

  • One line per test case
  • Output format must be exactly:

    Pair: <value> | <metadata>
    
  • Floating-point values must be printed with exactly two decimal places
  • Each output must appear on its own line

Example:

Input

3
f-i 25.5 1001
i-c 1 E
i-i 4095 1

Output

Pair: 25.50 | 1001
Pair: 1 | E
Pair: 4095 | 1 

Constraints:

  • 1 ≤ N ≤ 20
  • No dynamic memory allocation
  • Must use template <typename T1, typename T2>
  • Use only standard C++ headers
  • Solution must compile cleanly with a standard C++ compiler

 

 

 

 

Need Help? Refer to the Quick Guide below

Templates allow you to write a single "blueprint" for a function or class that can work with any data type. Instead of writing swap_int, swap_float, and swap_double, you write one swap<T> function.

This is Compile-Time Polymorphism. The compiler sees how you use the template and generates (instantiates) the specific code for those types. It’s like a "Smart Macro" that respects type safety and scope.

Syntax & Usage

1. Function Templates

Write logic once, use it for different types.

// Template Declaration
// 'T' is a placeholder for a type (int, float, struct...)
template <typename T> 
void swap_values(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap_values(x, y);     // Compiler generates: void swap_values(int&, int&)

    float f1 = 1.5, f2 = 4.5;
    swap_values(f1, f2);   // Compiler generates: void swap_values(float&, float&)
}

2. Class Templates

Crucial for creating generic data structures (Buffers, Queues, Linked Lists) where the logic is the same regardless of what data is stored inside.

// A Circular Buffer that can hold 'T' type objects
// 'Size' is a Non-Type Template Parameter (Constant Integer)
template <typename T, int Size>
class RingBuffer {
private:
    T buffer[Size]; // Array size fixed at compile time
    int head = 0;

public:
    void push(T val) {
        buffer[head] = val;
        head = (head + 1) % Size;
    }
};

// Usage
RingBuffer<int, 64> adc_buf;       // Buffer of 64 integers
RingBuffer<float, 10> sensor_avg;  // Buffer of 10 floats
RingBuffer<Packet, 128> uart_rx;   // Buffer of 128 custom Structs

How It Works (Template Instantiation)

Templates do not exist in the final binary until they are used.

When you write RingBuffer<int, 64>, the compiler acts like a code generator:

  1. It takes the "Blueprint".

  2. It replaces T with int and Size with 64.

  3. It compiles that new specific class.

Code

Compiler Action

Memory Usage

sort<int>(arr)

Generates sort_int(int* arr)Code size increases by size of function.

sort<float>(arr)

Generates sort_float(float* arr)Code size increases again.

sort<int>(arr2)

Reuses existing sort_intNo new code generated.

Specialized Templates (Template Specialization)

Sometimes generic code works for 99% of types, but fails for one specific type (like bool or char*). You can provide a special version for that type.

// Generic
template <typename T>
T add(T a, T b) { return a + b; }

// Specialization for 'char' (e.g., to prevent overflow or change logic)
template <>
char add<char>(char a, char b) {
    // Saturated addition logic for 8-bit audio
    int res = a + b;
    if (res > 127) return 127; 
    return (char)res;
}

Relevance in Embedded/Firmware

  • Type-Safe Drivers (Zero Cost Abstraction)

    In C, drivers often use void* buffers and require casting (dangerous) or macros (hard to debug).

    Templates give you type safety with zero runtime overhead. The compiler resolves types during compilation, so the resulting assembly is as fast as hand-written C code.

  • Compile-Time Configuration

    You can use "Non-Type Template Parameters" (integers, pointers) to configure hardware.

    template <uint32_t BaseAddr>
    class GPIO {
    public:
        static void setHigh() { 
            *(volatile uint32_t*)(BaseAddr) = 1; 
        }
    };
    
    using LED_Port = GPIO<0x40021000>;
    // LED_Port::setHigh() compiles to a direct memory write instruction!
  • Avoids malloc

    Standard containers (std::vector) use the Heap (malloc). In embedded, we hate malloc.

    With templates, you can pass the size as a parameter (RingBuffer<int, 64>), allowing the buffer to be allocated on the Stack or BSS (Static Memory).

The "Code Bloat" Myth vs Reality

  • The Fear: "Templates generate a copy of the code for every type, filling up my Flash memory."

  • The Reality: Yes, vector<int> and vector<float> are two separate code blocks.

    • However, if you manually wrote IntVector and FloatVector structs in C, you would have the exact same amount of code.

    • Optimization: If different template instantiations result in identical assembly (e.g., MyClass<unsigned int> vs MyClass<int> on some architectures), the linker can sometimes merge them.

Common Pitfalls & Best Practices

Pitfall

Details

❌ Header Definitions

Template code must be implemented in the Header File (.h), not the Source File (.cpp).

Why? The compiler needs to see the entire source code to generate the specific version when it compiles main.cpp. If it's hidden in impl.cpp, you get Linker Errors.

❌ Cryptic Error Messages

If you make a typo in a template, the compiler vomits 100 lines of error garbage.

Tip: Always scroll to the very top error message; it usually points to the actual line. Ignore the "instantiated from here" chain below it.

❌ Bloat via Permutation

Be careful with integer parameters.

Delay<10>() and Delay<11>() create TWO separate functions. If you use Delay<N> with 100 different numbers, you generate 100 functions. Use function arguments (delay(int n)) for values that change often.

typename vs class

template <typename T> and template <class T> are identical. typename is preferred in modern C++ because T can be an int, which isn't technically a "class".