In previous lessons, you learned about class template specialization. This lesson explores a specific and practical application: creating partial specializations that handle pointer types differently from value types.

The pointer problem

Consider a simple cache template that stores a single value:

#include <iostream>

template <typename T>
class Cache
{
private:
    T m_value{};

public:
    Cache(T value) : m_value{value} {}

    void show() const
    {
        std::cout << m_value << '\n';
    }
};

int main()
{
    Cache<int> num{42};
    num.show();

    Cache<double> pi{3.14159};
    pi.show();

    return 0;
}

Output:

42
3.14159

This works perfectly for value types. But what happens when you use it with pointers?

int main()
{
    int x{100};
    int* ptr{&x};

    Cache<int*> cached{ptr};
    cached.show();

    return 0;
}

Output (when tested):

0x7ffd8c5a4e9c

The cache stores and displays the pointer's address, not the value it points to. This is technically correct but rarely useful - you usually want to cache the pointed-to value, not the address.

Attempting full specialization

You could create a full specialization for int*:

#include <iostream>

template <typename T>
class Cache
{
private:
    T m_value{};

public:
    Cache(T value) : m_value{value} {}

    void show() const
    {
        std::cout << m_value << '\n';
    }
};

template <>
class Cache<int*>
{
private:
    int* m_ptr{};

public:
    Cache(int* ptr) : m_ptr{ptr} {}

    void show() const
    {
        if (m_ptr)
            std::cout << *m_ptr << '\n';
        else
            std::cout << "null\n";
    }
};

int main()
{
    int x{100};
    Cache<int*> cached{&x};
    cached.show();

    return 0;
}

Output:

100

This works, but only for int*. What about double*, char*, or pointers to user-defined types? Creating a full specialization for every pointer type is impractical and unmaintainable.

Partial specialization for all pointer types

Partial template specialization solves this elegantly:

#include <iostream>

template <typename T>
class Cache
{
private:
    T m_value{};

public:
    Cache(T value) : m_value{value} {}

    void show() const
    {
        std::cout << m_value << '\n';
    }
};

template <typename T>
class Cache<T*>
{
private:
    T* m_ptr{};

public:
    Cache(T* ptr) : m_ptr{ptr} {}

    void show() const
    {
        if (m_ptr)
            std::cout << *m_ptr << '\n';
        else
            std::cout << "null\n";
    }
};

int main()
{
    int x{42};
    Cache<int*> intCache{&x};
    intCache.show();

    double d{3.14159};
    Cache<double*> doubleCache{&d};
    doubleCache.show();

    Cache<int*> nullCache{nullptr};
    nullCache.show();

    return 0;
}

Output:

42
3.14159
null

The partial specialization Cache<T*>:

  • Still has a template parameter T (the pointed-to type)
  • Specializes for any pointer type (T*)
  • Automatically dereferences pointers when displaying

Note that T represents the non-pointer type. For Cache<int*>, T is int, so m_ptr has type int*.

Why not a partially specialized function?

You might wonder why we need a class specialization instead of just specializing the member function:

// This doesn't work
template <typename T>
void Cache<T*>::show() const
{
    if (m_ptr)
        std::cout << *m_ptr << '\n';
}

This fails because it's attempting to partially specialize a member function, which isn't allowed in C++. When you call intCache.show(), you're calling a method of the class Cache<int*>, not instantiating a function template. To customize behavior for pointer types, you must specialize the entire class.

Handling ownership and lifetime

The partial specialization above has a subtle problem: it stores a raw pointer to an object it doesn't own. If that object is destroyed, the cache has a dangling pointer:

int main()
{
    Cache<int*> cached{nullptr};

    {
        int temp{999};
        cached = Cache<int*>{&temp};
        cached.show();  // OK - temp is alive
    }
    // temp is destroyed here

    cached.show();  // Undefined behavior - temp is gone!

    return 0;
}

There are several solutions:

Solution 1: Document reference semantics clearly

Make it explicit that Cache<T*> is a view, not an owner:

// Cache<T*> is a non-owning view.
// The caller must ensure the pointed-to object outlives the Cache.
template <typename T>
class Cache<T*>
{
private:
    T* m_ptr{};

public:
    Cache(T* ptr) : m_ptr{ptr} {}

    void show() const
    {
        if (m_ptr)
            std::cout << *m_ptr << '\n';
    }
};

This works but relies on careful usage and good documentation.

Solution 2: Prevent pointer instantiations

You can disallow pointer types entirely using static_assert:

#include <iostream>
#include <type_traits>

template <typename T>
class Cache
{
    static_assert(!std::is_pointer_v<T> && !std::is_null_pointer_v<T>,
                  "Cache cannot be instantiated with pointer types");

private:
    T m_value{};

public:
    Cache(T value) : m_value{value} {}

    void show() const
    {
        std::cout << m_value << '\n';
    }
};

int main()
{
    int x{42};

    Cache<int> good{x};       // OK
    good.show();

    // Cache<int*> bad{&x};   // Compile error
    // Cache<nullptr_t> bad2{nullptr};  // Compile error

    return 0;
}

This prevents pointer misuse at compile time. It's the safest option when pointer semantics don't make sense for your class.

Solution 3: Copy the pointed-to value

Make the specialized version own a copy of the value:

#include <iostream>
#include <memory>

template <typename T>
class Cache
{
private:
    T m_value{};

public:
    Cache(T value) : m_value{value} {}

    void show() const
    {
        std::cout << m_value << '\n';
    }
};

template <typename T>
class Cache<T*>
{
private:
    std::unique_ptr<T> m_value;

public:
    Cache(T* ptr)
        : m_value{ptr ? std::make_unique<T>(*ptr) : nullptr}
    {
    }

    void show() const
    {
        if (m_value)
            std::cout << *m_value << '\n';
        else
            std::cout << "null\n";
    }
};

int main()
{
    int x{42};
    Cache<int*> cached{&x};  // Makes a copy of x

    x = 999;  // Changing x doesn't affect cached

    cached.show();  // Still shows 42

    return 0;
}

Output:

42

This version makes a deep copy, so the cache owns its data and isn't affected by external changes. Using std::unique_ptr handles memory management automatically.

Practical example: Resource handle specialization

Partial specialization for pointers is useful when pointers represent resource handles:

#include <iostream>

template <typename T>
class ResourceHolder
{
private:
    T m_resource;

public:
    ResourceHolder(T resource) : m_resource{resource} {}

    const T& get() const { return m_resource; }
};

template <typename T>
class ResourceHolder<T*>
{
private:
    T* m_resource;
    bool m_owning;

public:
    ResourceHolder(T* resource, bool owning = false)
        : m_resource{resource}, m_owning{owning}
    {
    }

    ~ResourceHolder()
    {
        if (m_owning && m_resource)
        {
            std::cout << "Releasing resource\n";
            delete m_resource;
        }
    }

    ResourceHolder(const ResourceHolder&) = delete;
    ResourceHolder& operator=(const ResourceHolder&) = delete;

    T* get() const { return m_resource; }
};

int main()
{
    ResourceHolder<int> value{42};
    std::cout << "Value: " << value.get() << '\n';

    int* allocated{new int{100}};
    ResourceHolder<int*> resource{allocated, true};
    std::cout << "Resource: " << *resource.get() << '\n';

    return 0;
}

Output:

Value: 42
Resource: 100
Releasing resource

The pointer specialization adds ownership tracking, automatically cleaning up owned resources.

Consistency between value and pointer semantics

When creating pointer specializations, consider whether the value and pointer versions should have consistent behavior:

// Inconsistent - value version makes a copy, pointer version is a view
template <typename T>
class Container
{
    T m_data;  // Owns a copy
};

template <typename T>
class Container<T*>
{
    T* m_data;  // Just a view
};

This inconsistency can lead to bugs. Users might assume both versions have similar ownership semantics. Clear documentation or preventing pointer instantiation can avoid confusion.

When to use pointer specialization

Pointer specialization is valuable when:

  1. Dereferencing makes sense: The pointed-to value is more meaningful than the address
  2. Ownership needs differ: Pointers might need special resource management
  3. API consistency matters: You want MyClass<T> and MyClass<T*> to work similarly from the user's perspective

Avoid pointer specialization when:

  1. Pointer semantics are unclear: It's ambiguous whether the class should own or view the data
  2. Simple prohibition is better: The class shouldn't work with pointers at all
  3. The value version suffices: Users can dereference before passing to the template

Summary

Partial template specialization for pointers allows you to:

  1. Handle pointer types differently from value types transparently
  2. Dereference pointers automatically for more intuitive behavior
  3. Implement different ownership strategies for pointers

However, be mindful of:

  1. Lifetime and ownership issues with raw pointers
  2. Semantic consistency between value and pointer versions
  3. Whether preventing pointer instantiation might be simpler

Using static_assert to disallow pointers or std::unique_ptr to manage ownership are often the safest approaches for pointer specializations.