C++ Function and Class Templates: Build One Type-Safe Range Pattern

Build a reusable C++20 clamp function and Range class that work for int and double while catching mixed-type calls during compilation.

KnowledgeGate Team

Exam prep & CS education

Updated 29 Oct 20266 min read
C++ Function and Class Templates: Build One Type-Safe Range Pattern

Writing maxInt, maxDouble, IntRange and DoubleRange repeats the same logic under different type names. Replacing every type with auto does not create a reusable, type-checked abstraction. A function template clamps a value, and a class template owns valid bounds, so the same source handles integer marks and double temperatures while rejecting a mixed-type call during compilation. The pattern builds on functions, classes and constructors in Coding & DSA Courses for Placements. The broader Templates in C++: Function and Class Templates with Runnable Examples adds non-type parameters and specialisation, while the Range<T> and clamp_value<T> pair below isolates same-type deduction, bounds invariants and mixed-type rejection.

1. C++ templates are compile-time patterns with type parameters

A template is a source-code pattern parameterised by a type. template <typename T> introduces the type parameter T. The compiler substitutes a concrete type when the program uses the template, not when it creates an object at runtime.

A function template can generate functions such as clamp_value<int> and clamp_value<double>. A class template can generate types such as Range<int> and Range<double>. Runtime object creation happens later, after those concrete types have been formed.

Our safety rule is precise. All three clamp_value arguments must deduce the same T, and every Range<T> stores both bounds as that one type. Therefore, clamp_value(127, 0, 100) deduces int, while clamp_value(16.8, 18.5, 24.0) deduces double.

Templates are not macros. The compiler parses and type-checks the pattern. If deduction or a constraint fails, compilation stops instead of performing untyped text replacement.

2. Function template worked example: clamp one value into typed bounds

The pattern relies on the C++20 standard concepts std::totally_ordered and std::same_as. The function is:

cpp
#include <concepts>
#include <stdexcept>

template <std::totally_ordered T>
T clamp_value(T value, T low, T high) {
    if (high < low) {
        throw std::invalid_argument("low must not exceed high");
    }
    if (value < low) return low;
    if (high < value) return high;
    return value;
}

std::totally_ordered states the ordering ability expected from T. The shared parameter type is what blocks conflicting deduction. Now trace every branch:

  • clamp_value(127, 0, 100) returns 100 because 100 < 127 is true.

  • clamp_value(-8, 0, 100) returns 0 because -8 < 0 is true.

  • clamp_value(72, 0, 100) returns 72 because it lies inside the bounds.

  • clamp_value(16.8, 18.5, 24.0) returns 18.5 because 16.8 < 18.5 is true.

By contrast, clamp_value(72, 0.0, 100.0) does not compile. The first argument tries to deduce T as int, while the bounds try to deduce it as double. This is a compile-time deduction failure, not a runtime exception.

Compile-time template-instantiation flow showing clamp_value paths for int values 127, -8, and 72, double value 16.8, and a conflicting int-versus-double deduction.

3. Class template: make valid bounds a reusable type

The free function performs one operation. The class template is the stateful half of the pattern: it preserves valid bounds and offers behaviour that uses them. This separation between invariant and behaviour is also useful when studying Object Oriented Technology Explained.

cpp
template <std::totally_ordered T>
class Range {
    T low_;
    T high_;

public:
    Range(T low, T high) : low_{low}, high_{high} {
        if (high_ < low_) {
            throw std::invalid_argument("low must not exceed high");
        }
    }

    template <typename U>
        requires std::same_as<U, T>
    bool contains(U value) const {
        return !(value < low_) && !(high_ < value);
    }

    template <typename U>
        requires std::same_as<U, T>
    T clamp(U value) const {
        return clamp_value(value, low_, high_);
    }
};

After construction, the invariant is low_ is not greater than high_. Range<int> reversed{100, 0} has valid types but invalid values, so its constructor throws std::invalid_argument at runtime.

The member templates solve a different problem. Their U must be exactly T, so Range<int>::clamp(72.5) fails its compile-time constraint instead of silently narrowing a double to an int.

4. One pattern, two concrete types: run the complete worked program

The program uses class template argument deduction. Class template argument deduction uses the constructor arguments to form Range<int> and Range<double>.

cpp
#include <concepts>
#include <iostream>
#include <stdexcept>

template <std::totally_ordered T>
T clamp_value(T value, T low, T high) {
    if (high < low) {
        throw std::invalid_argument("low must not exceed high");
    }
    if (value < low) return low;
    if (high < value) return high;
    return value;
}

template <std::totally_ordered T>
class Range {
    T low_;
    T high_;

public:
    Range(T low, T high) : low_{low}, high_{high} {
        if (high_ < low_) {
            throw std::invalid_argument("low must not exceed high");
        }
    }

    template <typename U>
        requires std::same_as<U, T>
    bool contains(U value) const {
        return !(value < low_) && !(high_ < value);
    }

    template <typename U>
        requires std::same_as<U, T>
    T clamp(U value) const {
        return clamp_value(value, low_, high_);
    }
};

int main() {
    Range marks{0, 100};
    Range room_temperature{18.5, 24.0};

    std::cout << std::boolalpha;
    std::cout << marks.clamp(127) << '\n';
    std::cout << marks.contains(72) << '\n';
    std::cout << room_temperature.clamp(16.8) << '\n';
    std::cout << room_temperature.contains(22.6) << '\n';
}

The exact output is:

Code
100
true
18.5
true

For marks.contains(72), both 72 < 0 and 100 < 72 are false. Negating both checks makes the result true. For room_temperature.clamp(16.8), the comparison 16.8 < 18.5 is true, so the function returns the lower bound 18.5.

Two-column object-and-call figure showing Range<int> bounds 0 and 100, Range<double> bounds 18.5 and 24.0, and shared Range<T> and clamp_value<T> blueprints.

5. Deduction, explicit arguments and instantiation: what the compiler actually does

The process has three useful stages:

  1. Deduction selects T from function-call or constructor arguments.

  2. Constraint checking confirms that the selected type supports the promised operations.

  3. Instantiation forms the concrete function or class required by the program.

This program uses clamp_value<int>, clamp_value<double>, Range<int> and Range<double>. One checked source pattern produces the exact specialisations that are used.

An explicit argument changes deduction. clamp_value<double>(72, 0.0, 100.0) is valid and returns 72.0 because choosing T=double deliberately allows 72 to convert. Templates do not forbid every implicit conversion. They make the chosen type and its checks visible to the compiler.

Other languages provide generic facilities with different syntax and rules. The Programming Languages Survey is a broader comparison route, but this example stays with C++ template behaviour.

6. Function and class template mistakes, why they fail and the repair

Tempting move

What goes wrong

Repair

Call clamp_value(5, 1.0, 9.0)

Deduction conflicts between int and double

Use one argument type, or explicitly choose T when conversion is intentional

Omit template <...>

The compiler sees an ordinary declaration with an unknown type

Put the template header immediately before the function or class

Define a class-template member outside the class without its template header

The definition is no longer tied correctly to the class template

Repeat the class and member template headers with the qualified name

Treat typename T as a runtime variable

T names a type, not stored data

Declare an object separately, such as T value

Supply a type without the required ordering operations

std::totally_ordered<T> is not satisfied

Implement the required comparisons or use a suitable type

Confuse bounds 100, 0 with invalid types

Deduction succeeds, then the runtime invariant fails

Validate bound values in the constructor

Keep template definitions visible to any translation unit that instantiates them, commonly by placing them in a header. Otherwise, the linker may report an unresolved specialisation. Also avoid Range<int>{0.5, 100.5}, whose braced initialisation attempts narrowing. Express the intended type directly as Range<double>{0.5, 100.5}.

Separate specialisations can increase generated code size. Templates remove repeated source logic, but they do not guarantee one shared machine-code body, a smaller binary or safe operations beyond the constraints and checks actually written.

7. How interviews and exam-style questions test C++ templates

Representative questions ask you to predict which calls compile, infer a deduced type, distinguish function and class templates, trace an instantiated call, find a missing template header, or explain a failed constraint. These are question forms, not claims about any current syllabus or paper pattern.

Try three answer-first checks:

  • clamp_value(5, 1, 9) deduces int and returns 5.

  • clamp_value(5, 1.0, 9.0) fails deduction because T conflicts.

  • Range limits{2.5, 7.5} deduces Range<double>, so limits.clamp(9.0) returns 7.5.

8. C++ function and class templates: the short version and next step

  • A function template generates functions.

  • A class template generates types.

  • Deduction and constraint checks happen at compile time.

  • Object invariants such as low <= high still depend on runtime values.

Rebuild the example from memory. Write clamp_value<T>, place the bounds in Range<T>, then verify the four outputs: 100, true, 18.5, true. If clamp_value(72, 0.0, 100.0) compiles in your version, check whether you explicitly forced a type or changed the parameter design.

The C++ Programming Course is the next structured step for C++ concepts, MCQs and coding questions.