Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Pointer Type Specializations
Learn partial template specialization for pointers and understand memory management concepts.
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:
- Dereferencing makes sense: The pointed-to value is more meaningful than the address
- Ownership needs differ: Pointers might need special resource management
- API consistency matters: You want
MyClass<T>andMyClass<T*>to work similarly from the user's perspective
Avoid pointer specialization when:
- Pointer semantics are unclear: It's ambiguous whether the class should own or view the data
- Simple prohibition is better: The class shouldn't work with pointers at all
- The value version suffices: Users can dereference before passing to the template
Summary
Partial template specialization for pointers allows you to:
- Handle pointer types differently from value types transparently
- Dereference pointers automatically for more intuitive behavior
- Implement different ownership strategies for pointers
However, be mindful of:
- Lifetime and ownership issues with raw pointers
- Semantic consistency between value and pointer versions
- 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.
Pointer Type Specializations - Quiz
Test your understanding of the lesson.
Practice Exercises
Value Holder with Pointer Support
Create a `Holder` class template that stores values. Partially specialize it for pointer types to automatically dereference pointers when displaying, and handle null pointers safely.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!