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:
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.