C++ template argument type deduction
In C++17, you can have auto
non-type template parameters. This will let you solve your problem.
Something like:
template<auto object, class T=std::decay_t<decltype(*object)>>
int Function();
(assuming you want the type T
within the body of Function
)
In C++14, the C++17 feature is missing. It was added exactly because it was missing. Workarounds involve macros like #define UGLY_HACK(...) decltype(__VA_ARGS__), __VA_ARGS__
.
Note: The answer here has been borrowed from effective modern C++ with a (very) few additions of my own
This is one of those questions that are easy to pose but difficult to answer! I remember reading an entire ch. apter on template type deduction, and to a rookie reader, the answer is not clear in one read either. Nevertheless, I will try to clarify it here.
One should note that there is something called Universal References (which are not the same as references or r-value references) that influences template type deduction, and I assume readers know about l-value and r-value references.
Any ubiquitous function template definition looks like the following:
template <typename T>
returnType function(paramType param);
A call to function would look somehow look like this :
function(expression);
The compiler uses expression to determine the type of T and the type of paramType. This is so because more often paramType contains decorations like const, const&, const&&, etc. Beginners would be tempted to believe that the type T deduced by the compiler will be the same as the type of expression, i.e., the argument passed to the function, but it is not always the case. Deduction of type T depends both on expression and paramType . Depending on what the function parameter paramType is there are three cases to be considered for template type deduction:
- paramType is pointer or reference but not a universal reference.
- paramType is a universal reference.
- paramType is neither a pointer nor a reference.
Let's take a look at each case one by one
Case 1: paramType is a pointer or a reference but not a universal reference
Call me crazy, but this is the simplest case that can be encountered. In this case, type deduction works like this: (i) If expression is a reference, then ignore the reference part (ii) then match expression's pattern against paramType to determine T
Lets take a look at an example :
template <typename T>
returnType function(T ¶m);
We have the following variable declarations:
int x = 23; // x is int
const int const_x = x; // const_x is const int
const int& ref_x = x; // ref_x is a reference to x as const int
The deduced call for T and param in various calls are as follows :
f(x); //T is int, param's type is int&
f(const_x); //T is const int, param's type is const int&
f(ref_x); //T is const int, param's type is const int&
There are two points to be noted here:
(i) the compiler ignores the reference-ness for type deduction here
(ii) the const-ness becomes a part of type T when passing a const object or reference to a const object, and hence passing const objects or references to const object to functions taking parameter T& is safe.
If we change the function parameter from T& to const T&, because in this case we are assuming param to be reference to const, the const-ness need not be deduced as a part of T . Below is an example:
template <typename T>
returnType function(const T& param); // param is now a ref-to-const
int x = 23; // same as previous
const int const_x = x; // same as previous
const int& ref_x = x; // same as previous
f(x); // T is int, paramType is const int&
f(const_x); // T is int, paramType is const int&
f(ref_x); // T is int, paramType is const int&
Note: variable 'x' is not a const argument to 'f()' but it is till deduced as a const param
If paramType is a pointer, things will work fundamentally the same way as with references. There will be pointers instead of references. E.g., below for the sake of completeness is provided:
template <typename T>
returnType function( T* paramType); // paramType is now a pointer
int x = 23; // same as before
const int *pointer_x = &x; // pointer_x is pointer to x as const int
f(&x); // T is int, paramType is int*
f(pointer_x); // T is const int, paramType is const int*
For the sake of completeness I may as well post the case if paramType were a pointer to a constant object like the following:
template <typename T>
returnType function(const T* paramType);
int x = 23; // same as before
const int *pointer_x = &x; // pointer_x is pointer to x as const int
f(&x); // T is int, paramType is const int*
f(pointer_x); // T is int, paramType is const int*
i.e., again the const-ness is not anymore deduced as a part of T
In case of r-value references, type T and paramType deduction follow essentially the same rules as they do in case of l-value references.
This covers most of it for the first case. Let's look at our case 2.
Case 2: paramType is a universal reference
Universal references are declared like r-value references but take l-value, but what makes their behavior different is that the function arguments receive l-value references. Here's how the type deduction works for this case:
(i) If expression is an l-value, both T and paramType are deduced to be l-value. (This seems strange in the face of how the code looks like because although paramType is declared using the syntax of r-value reference, its deduced type is of l-value reference.) It should be noted that this is the only case where T is deduced to be a reference.
The example below clarifies my explanation:
template <typename T>
returnType function(T&& paramType); // param becomes universal reference if
// argument to function call is an l-value
int x = 23 // same as previous
const int const_x = x; // same as previous
const int& ref_x = x; // same as previous
f(x); // x is l-value therefore T is int&
// paramType is int&
f(const_x); // const_x is l-value therefore T is const int&
//paramType is also const int&
f(ref_x); // ref_x is l-value therefore T is const int&
// paramType is also const int&
f(23); // 23 is r-value so T is int
// paramType is now int&&
I want to be honest here and say that this doesn't explain why universal references work the way they do, but I think this post will become too lengthy if I go on to justify it here.
Case 3: paramType is neither a pointer nor a reference
This is where pass-by-value in template occurs, which implies that param will be a copy of whatever is passed to the argument of the calling function, i.e., a completely new object, and this motivates the rules that govern type deduction of T from expression . Two points to be noted here are:
(i) ignore the refrence-ness in expression , if there happens to be one.
(ii) after ignoring the ref-ness, ignore const-ness or volatile-ness too, i.e if present
template <typename T>
returnType function(T paramType);
int x = 23;
const int const_x = x;
const int& ref_x = x;
f(x); // T and paramType are both int
f(const_x); // T and paramType are both int here too
f(ref_x); // T and paramType are both int again
Note even though const_x and ref_x are const objects which cannot be modified, it doesn't mean that their copies cannot be modified. This looks straightforward, but it gets tricker when we pass a constant pointer to a constant object. Let's take a look at another example:
template <typename T>
returnType function(T param);
const double *const dPtr = 23; // dPtr is const pointer to const double
function(dPtr); // passing argument of type const double *const
When const pointer is passed by value, the const-ness is lost, and the pointer is copied by value, which is in sync with the type deduction rules for pass by value, but the const-ness of what pointer points to is preserved, and hence the paramType will be const *double.
This might get your head spinning as it did to me when I started to learn about it. The best way would be to re-read it and try to code it.