I'm reading in my book about conversions and template type parameters, and the following is basically confusing the heck out of me (I wrote my questions within the paragraphs that I quoted):
"template <typename T> T fobj(T, T);
template <typename T> T fref(const T&, const T&);
int a[10], b[42];
fobj(a,b); //calls f(int*, int*)
fref(a,b); //error: array types don't match
"In [this] pair of calls, we pass array arguments in which the arrays are different sizes and hence have different types. [My question: since when are arrays of different sizes considered different types? Aren't both of these arrays type "int" arrays? What does it mean by this?] In the call to fobj, the fact that the array types differ doesn't matter. Both arrays are converted to pointers. The template parameter type in fobj is int*. The call to fref, however, is illegal. When the parameter is a reference, the arrays are not converted to pointers. The types of a and b don't match, so the call is in error. [Question 2: again, how do the types of a and b not match, and why exactly is this call illegal? I'm not understanding] "
Q1 since when are arrays of different sizes considered different types?
Since always. What it means is that these two arrays have different type:
int a[10];
int b[42];
Q2 how do the types of a and b not match, and why exactly is this call illegal? I'm not understanding
They have different type, explicitly a has type int[10] and b has type int[42]. The type can decay to int* in certain contexts, such as when passed into functions expecting int*. This is what happens in the first function template in the code you quoted. The template gets instantiated into something like
int* fobj(int*, int*);
So far so good. However, a function taking a and b by reference would need a parameter list such as
int foo( int const (&array1)[10], int const (&array2)[42] );
int const (&arr)[N] is just the syntax for "const reference to a size N array of ints". A function template that could accept two arrays with elements of the same type but arbitrary size would then be
template <typename T, size_t N, size_t M>
T fref(T const (&)[N], T const (&)[M]);
When you instantiate this function with a and b, you will get a function like foo() above.
Related
Consider this pseudo code for a type deduction case:
template<typename T> void f(ParamType param);
Call to function will be:f(expr);
According to type deduction case where ParamType is not a reference, pointer, nor a universal reference
(see S. Meyers "Effective Modern C++", p.14), but passed by value, to determine type T, one needs firstly
to ignore the reference and const part of 'expr' and then pattern-match exprs type to determine T.
The driver will be:
void PerformTest() {
int i = 42;
int* pI = &i;
f_const_left(pI);
f_non_template_left(pI);
f_const_right(pI);
f_non_template_right(pI);
}
Now consider these functions, which, using this deduction rule, are showing some counter-intuitive results while being called with pointer as an argument:
template<typename T> void f_const_left(const T t) {
// If 'expr' is 'int *' then, according to deduction rule for value parameter (Meyers p. 14),
// we need to get rid of '&' and 'const' in exp (if they exist) to determine T, thus T will be 'int *'.
// Hence, ParamType will be 'const int *'.
// From this it follows that:
// 1. This function is equivalent to function 'func(const int * t){}'
// 2. If ParamType is 'const int *' then we have non-const pointer to a const object,
// which means that we can change what pointer points to but cant change the value
// of pointer address using operator '*'
*t = 123;// compiler shows no error which is contradiction to ParamType being 'const int *'
t = nullptr; // compiler shows error that we cant assign to a variable that is const
// As we see, consequence 2. is not satisfied:
// T is straight opposite: instead of being 'const int *'
// T is 'int const *'.
// So, the question is:
// Why T is not 'const int*' if template function is f(const T t) for expr 'int *' ?
}
Consider consequence 1.:
Lets create an equivalent non-template function:
void f_non_template_left(const int* t) {
// 1. Can we change the value through pointer?
*t = 123; // ERROR: expression must be a modifiable lvalue
// 2. Can we change what pointers points to?
t = nullptr; // NO ERROR
// As we can see, with non-template function situation is quite opposite.
}
For for completeness of the experiment, lets also consider another pair of functions but with 'const' being placed from the right side of a T: one template function and its non-template equivalent:
template<typename T> void f_const_right(T const t) {
// For expr being 'int *' T will be 'int *' and ParamType will be 'int * const',
// which is definition of a constant pointer, which cant point to another address,
// but can be used to change value through '*' operator.
// Lets check it:
// Cant point to another address:
t = nullptr; // compiler shows error that we cant assign to a variable that is const
// Can be used to change its value:
*t = 123;
// So, as we see, in case of 'T const t' we get 'int * const' which is constant pointer, which
// is intuitive.
}
Finally, the non-template function with 'const' from the right side of type:
void f_non_template_right(int* const t) {
// 1. Can we change the value through pointer?
*t = 123; // No errors
// 2. Can we change what pointers points to?
t = nullptr; // ERROR: you cant assign to a variable that is const
// As we can see, this non-template function is equivalent to its template prototype
}
Can someone explain why there is such insonsistency between template and non-template functions ?
And why template function with 'const' on the left is behaving not according to the rule of deduction?
// Hence, ParamType will be 'const int *'.
No, it will not (and not just because ParamType is not used in the code. For the purposes of this answer, let's pretend you meant T, which is there).
int * is a sequence of two tokens. One of those tokens is a typename (int), and the other is *. When combined in this way, the two tokens name a single type: pointer to an int.
const int is a sequence of two tokens. When combined, they name the type: const int.
const int* is a sequence of 3 tokens. When combined as such, they name a single type. The rules of C++ type naming are such that the const applies "const-ness" to the type specified by whatever is to the immediate left of it. If nothing is to its left, it applies to what is immediately to the right. So if you consider this as an expression, it is read as (const int)*. Therefore, this token sequence names the type: pointer to a const int.
const T, where T is a typename (which could be a template parameter, a type-alias, or the name of a type), is a sequence of two tokens. When combined, they name a single type: const (whatever type T designates).
If the type T happens to name is int*, then the type T designates is 'pointer to int', as previously discussed. Therefore, const T designates 'const (pointer to int)' Note that the pointer is what is const, not the int being pointed to.
Substitution of type aliases and template parameters is not done by copying-and-pasting tokens. It is done by applying the rules of typenames to the alias as a single unit. Whatever T is, the qualifiers like constapply to T as a unit.
int * const is a sequence of three tokens. Per the aforementioned rules, const is applied to whatever is to its left, if anything. So const applies to the *. Therefore, these tokens name the type: const pointer to int.
T const is a sequence of two tokens. When combined as such, they name a single type: const (whatever type T designates).
This is why T const and int * const behave the same, but const T and const int * don't.
(Referencing the C++14 Standard)
Your f_non_template_* functions aren't entirely correct.
Since T is a template parameter, it'll behave as if it is a unique type:
14.5.6.2 Partial ordering of function templates
(3) To produce the transformed template, for each type, non-type, or template template parameter (including
template parameter packs (14.5.3) thereof) synthesize a unique type, value, or class template respectively
and substitute it for each occurrence of that parameter in the function type of the template.
So to correctly test this your non-template functions would need to defined like this:
using TT = int*;
void f_non_template_left(const TT t) {
/* ... */
}
void f_non_template_right(TT const t) {
/* ... */
}
godbolt example
at which point you'll get exactly the same behaviour as with the templated functions.
Why it works that way
In this case T would be deduced to int*, which as a unique type would be a compound type:
3.9.2 Compound types
(1) Compound types can be constructed in the following ways:
[...]
(1.3) — pointers to void or objects or functions (including static members of classes) of a given type
[...]
And the cv-rules for compound types are as following:
3.9.3 CV-qualifiers
(1) A type mentioned in 3.9.1 and 3.9.2 [Compound Type] is a cv-unqualified type. Each type which is a cv-unqualified complete
or incomplete object type or is void (3.9) has three corresponding cv-qualified versions of its type: a constqualified version, a volatile-qualified version, and a const-volatile-qualified version.
(2) A compound type (3.9.2) is not cv-qualified by the cv-qualifiers (if any) of the types from which it is compounded. Any cv-qualifiers applied to an array type affect the array element type, not the array type (8.3.4).
So your cv-qualifiers for T in your template function refer to the top-level constness of the compound-type T in both your cases, so
template<typename T> void f_const_left(const T t);
template<typename T> void f_const_right(T const t);
are actually equivalent.
The only exception to this would be if T would be an array-type, in which case the cv-qualifier would apply to the elements of the array instead.
If you want to specify the constness of the pointed-to value, you could do it like this:
//const value
template<class T>
void fn(const T* value);
// const pointer
template<class T>
void fn(T* const value);
// const value + const pointer
template<class T>
void fn(const T* const value);
As I know, there are no arrays of references in C++. For example, a sentence like int& arr[20]; makes compilation error.
However, in the book Effective Modern C++, I saw the following code:
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T(&)[N])
{ ~~~~~~~
return N;
}
I cannot understand how, and why is this parameter declaration allowed.
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
int mappedVals[arraySize(keyVals)];
And if this sentence is executed, What type is T deduced to? Effective Modern C++ says T is deduced as a real array type if the function template has a reference type parameter. So the type of T is int[7]? Then N's type is int&[7]? It's confusing.
With the usage of (&), the parameter type is declared as a reference, not an array (or pointer). Then T(&)[N] declares a reference to array, which has N elements of type T.
When you pass keyVals, T is deduced as the element type of the array i.e. int; N is deduced as the size of the array i.e. 7. Note that deducing the size of array is only possible when passing array by reference; otherwise the size won't be reserved because of the array-to-pointer decay.
I was looking at the emulated version of nullptr and saw this converting operator (a member of nullptr_t):
template<class C, class T> // or any type of null
operator T C::*() const // member pointer...
{ return 0; }
This syntax for pointer to member function confuses me. I usually expect to see such a type as something like
R (C::*)(I1, I2, ...)
With the template above, there's no input arguments. I can't figure out how the type deduction works in this case. I'm having trouble forming a specific question, other than, how does this work? If I have code like this:
typedef int (MyClass::*MyTypedef)(float);
MyTypedef m = nullptr;
I'm guessing T deduces to int, and C deduces to MyClass. What "happens" to float?
That is a pointer to member, not necessarily a pointer to member function. The difference is that it can generate a pointer to member function or a pointer to non-function member.
Now in the particular use case, the destination is a pointer to member, the compiler is seeing an expression in which it needs a int (MyClass::*)(float), and on the other hand it has a nullptr. It tries to find a conversion and it finds the operator T C::*(), which is a valid conversion if C is deduced to be MyClass and T is deduced to be int (float) [function taking a float and returning an int].
I also find this particular corner of the language a bit confusing (having typedefs, or deduced types for functions), for example this is legal if weird:
typedef void int_f(int);
struct X {
int_f m;
};
void X::m(int x) { std::cout << x << '\n'; }
The same thing is going on in the conversion operator that you are concerned with.
From C++ Primer, I know that for template's parameter arguments, only two kinds of conversions are performed: one is const conversion, another one is array/function to pointer conversion.
However, when it comes to explicit argument, it seems that everything changes.
Assume we have a template function:
template <typename T>
int compare(const T &a, const T &b)
{
// do comparison
}
If no explicit argument involved, function call like this is illegal:
compare("foo", "foobar");
The weird thing happens (actually, it might not be weird but I do not understand) when we explicitly do:
compare<std::string>("foo", "foobar");
It seems that in the second call, "foo" and "foobar" are converted to std::string, which is controversial.
Is there any special rules for template explicit arguments? Thanks.
In the first case the compiler tries to deduce the type T from the given parameters. From the first parameter the compiler deduces the type const char (&)[4] (aka. reference to an array of 4 characters), from the second it gets const char (&)[7]. The two types don't match and the compiler can't figure out what T should be.
In the second example you explicitly specify that the T template parameter should be std::string. So T will be std::string. The compiler accepts the type you give and checks if the given function parameters match that type. In this case the parameters fit, because "foo" and "foobar" can be implicitly converted to std::string. (The const char[] degrade to const char*, and then there is a constructor that can construct a std::string from a const char*)
Please consider this code:
#include <iostream>
template<typename T>
void f(T x) {
std::cout << sizeof(T) << '\n';
}
int main()
{
int array[27];
f(array);
f<decltype(array)>(array);
}
Editor's Note: the original code used typeof(array), however that is a GCC extension.
This will print
8 (or 4)
108
In the first case, the array obviously decays to a pointer and T becomes int*. In the second case, T is forced to int[27].
Is the order of decay/substitution implementation defined? Is there a more elegant way to force the type to int[27]? Besides using std::vector?
Use the reference type for the parameter
template<typename T> void f(const T& x)
{
std::cout << sizeof(T);
}
in which case the array type will not decay.
Similarly, you can also prevent decay in your original version of f if you explicitly specify the template agument T as a reference-to-array type
f<int (&)[27]>(array);
In your original code sample, forcing the argument T to have the array type (i.e. non-reference array type, by using typeof or by specifying the type explicitly), will not prevent array type decay. While T itself will stand for array type (as you observed), the parameter x will still be declared as a pointer and sizeof x will still evaluate to pointer size.
The behaviour of this code is explained by C++14 [temp.deduct.call]:
Deducing template arguments from a function call
Template argument deduction is done by comparing each function template parameter type (call it P) with the type of the corresponding argument of the call (call it A) as described below
and then below:
If P is not a reference type:
If A is an array type, the pointer type produced by the array-to-pointer standard conversion (4.2) is used in place of A for type deduction;
For the call f(array);, we have A = int[27]. A is an array type. So the deduced type T is int *, according to this last bullet point.
We can see from the qualifier "If P is not a reference type" that this behaviour could perhaps be avoided by making P a reference type. For the code:
template<typename T, size_t N>
void f(T (&x)[N])
the symbol P means T(&)[N], which is a reference type; and it turns out that there are no conversions applied here. T is deduced to int, with the type of x being int(&)[N].
Note that this only applies to function templates where the type is deduced from the argument. The behaviour is covered by separate parts of the specification for explicitly-provided function template parameters, and class templates.
You can also use templates like the following:
template <typename T, std::size_t N>
inline std::size_t number_of_elements(T (&ary)[N]) {
return N;
}
This little trick will cause compile errors if the function is used on a non-array type.
Depending on your use case, you can work around that using references:
template<typename T>
void f(const T& x) {
std::cout << sizeof(T);
}
char a[27];
f(a);
That prints 27, as desired.