There is a std::
type trait helper. If you give it a pointer type T*
, its type
member type is T
. Otherwise, the type
member type is just the template type unchanged.
But what if you want to remove all pointers? For example, remove_
should be int
.
You can define this as a recursive operation. In pseudo-code:
template<typename T> auto remove_all_pointers { if (std::is_pointer_v<T>) { return remove_all_pointers< std::remove_pointer_t<T> >; } else { return T; } }
One way to express conditional evaluation in template metaprogramming is to use std::conditional<a, b, c>::type
, which is b
if a
is true
and is c
if a
is false
.
Therefore, your first attempt might be to write it as a one-liner built out of std::conditional
.
template<typename T> using remove_all_pointers_t = std::conditional_t< std::is_pointer_v<T>, remove_all_pointers_t< std::remove_pointer_t<T>>,; T>;
Okay, this doesn’t work because of the recursive reference to remove_
before it has completed its declaration. We can sidestep this by using a struct
.
template<typename T> struct remove_all_pointers { using type = std::conditional_t< std::is_pointer_v<T>, typename remove_all_pointers< std::remove_pointer_t<T>>::type, T>; };
This compiles, but you get an error when you try to use it:
using test = remove_all_pointers<int*const*volatile*>::type; // gcc In instantiation of 'struct remove_all_pointers<int>': recursively required from 'struct remove_all_pointers<int* const* volatile>' required from 'struct remove_all_pointers<int* const* volatile*>' required from here error: invalid use of incomplete type 'struct remove_all_pointers<int>' | using type = std::conditional_t< | ^~~~ note: definition of 'struct remove_all_pointers<int>' is not complete until the closing brace | struct remove_all_pointers | ^~~~~~~~~~~~~~~~~~~ // clang error: no type named 'type' in 'remove_all_pointers<int>' | typename remove_all_pointers< | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | std::remove_pointer_t<T>>::type, | ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~ note: in instantiation of template class 'remove_all_pointers<int>' requested here | typename remove_all_pointers< | ^ note: in instantiation of template class 'remove_all_pointers<int *const>' requested here note: in instantiation of template class 'remove_all_pointers<int *const *volatile>' requested here note: in instantiation of template class 'remove_all_pointers<int *const *volatile *>' requested here | using test = remove_all_pointers<int*const*volatile*>::type; | ^ // msvc error C2146: syntax error: missing '>' before identifier 'type' note: the template instantiation context (the oldest one first) is note: see reference to class template instantiation 'remove_all_pointers<int *const *volatile *>' being compiled note: see reference to class template instantiation 'remove_all_pointers<int *const *volatile >' being compiled note: see reference to class template instantiation 'remove_all_pointers<int *const >' being compiled note: see reference to class template instantiation 'remove_all_pointers<int>' being compiled
Okay, maybe we were too ambitious.
All the error messages show that the template was able to recurse and strip away pointers, but then it ran into a problem when it reached the base case. Let’s look at that base case:
struct remove_all_pointers<int> { using type = std::conditional_t< std::is_pointer_v<int>, remove_all_pointers< std::remove_pointer_t<int>>::type, int>; };
After substituting std::remove_pointer_t<int> = int
, we get
struct remove_all_pointers<int>
{
using type = std::conditional_t<
std::is_pointer_v<int>,
remove_all_pointers<int>::type,
int>;
};
Now we see the problem. The definition of remove_
is dependent on itself.
The catch here is that std::conditional
is not a short-circuiting operator. How can it be? It’s a template!
In order to instantiate a template, the compiler first evaluates the template parameters, and then it looks at the template expansion that results. The compiler doesn’t “look ahead” and say, “Oh, I can tell that the template expansion never uses its second parameter, so I will skip the evaluation of the second parameter.”¹
One way to solve this problem is to move the expansion of the two parameters to a partial specialization. That way, only the pointer cases invoke the template recursively.
template<typename T, bool = std::is_pointer_v<T>> struct remove_all_pointers; template<typename T> struct remove_all_pointers<T, false> { using type = T; }; template<typename T> struct remove_all_pointers<T, true> { using type = typename remove_all_pointers< std::remove_pointer_t<T>>::type; };
We add a hidden second template parameter which defaults to std::is_pointer_v<T>
. We then partially specialize the template on that second template parameter: If it’s false
(T
is not a pointer), then the type is T
itself, which provides our base case (no longer accidentally referring to itself). If it’s true
(T
is a pointer), then the type is calculated recursively after stripping away one layer of indirection.
template<typename T> using remove_all_pointers_t = typename remove_all_pointers<T>::type; static_assert(std::is_same_v< remove_all_pointers_t<int*const*volatile*>, int>);
As a small tuning step, we can fold the base case into the initial definition, so that only the recursive case is a partial specialization.
template<typename T, bool = std::is_pointer_v<T>> struct remove_all_pointers { using type = T; }; template<typename T> struct remove_all_pointers<T, true> { using type = typename remove_all_pointers< std::remove_pointer_t<T>>::type; };
Are we done?
No, not yet.
We’ll continue next time.
¹ Indeed, the “I evaluate all the parameters even if they aren’t used” behavior is one of the things that SFINAE relies on!
My attempt would have been:
<code>
This works when you can use partial specialization, but sometimes the recursive template thing you want to do can’t be implemented using partial specialization. Though, maybe C++20 constraints might come to the rescue somehow? I haven’t experimented with them much yet.
Sounds like a job for std::type_identity, much like how I suggested in another comment on that prior post.