Let's consider those definitions:

/*** full type information with typeid ***/
template <class> class Type{};

template <class T> std::string typeStr()
{ return typeid(Type<T>).name(); }

/*** function template for parameter deduction ***/
template <class T> void func(const T &a)
{
    std::cout << "Deduced type for T is: " << typeStr<T>() << std::endl;
    std::cout << "\targument type is: " << typeStr<decltype(a)>() << std::endl;
}

with pointers to const

If the following statements are executed:

const int i=5, *ip=&i;
func(ip);

The output is:

Deduced type for T is: 4TypeI**PKi**E

So T is actually deduced as a pointer to a constant integer. The fact that the argument is a reference-to-const does not change the deduction, which is what one would expect because the constness of the pointer is low-level.

but with array of const

Nonetheless, if following statements are executed:

const int ia[3] = {3, 2, 1};
func(ia);

The output is:

Deduced type for T is: 4TypeI**A3_i**E

So T is actually deduced as an array of 3 non-const integers. The fact that the argument is a reference-to-const does change the deduction, as if the const was slipping into the array elements.

Actually, versions of CL up to 18 were deducing T as array of 3 const integers was what I expected to be standard, but it seems that since v19 it converged to what GCC and Clang are doing (i.e., deducing as non-const).

Thus, I assume the later behaviour to be standard, but was is the rationale ? It could seem surprising that it does not behave like with pointers.


Edit: Following dip comment, I will report here pointers to CWG issues related to this behaviour, pointers he actually posted as a comment on this answer (answer that actually raised this new question... C++ feels like a deep tunnel)

1

There are 1 answers

8
Apples On

Using this function template prototype:

template <typename T> void func(const T& a);

In your first example, the type deduction works as:

const int* ip;

func(ip) => func<const int*>(const (const int*)& a)
                 ^^^^^^^^^^         ^^^^^^^^^^

Note: This is pseudocode. The full type is const int* const&.

Note that the const int remains const int, but the * becomes * const.

This is because const int* is just a regular, mutable, non-volatile pointer. It is just a *. What it points to is irrelevant.

But in the second example, you have:

const int ia[3];

func(ia) => func<int[3]>(const (int[3])& a)
                 ^^^^^^         ^^^^^^

Note: This is pseudocode. The real type would be const int (&a)[3].

So the type deduction is working the same in both cases, discarding the outer const.

It so happens that a const array is the same as an array of const elements.

It might help to write types like this:

template <typename T> func(T const & a);

int const * ip;

func(ip) => func<int const *>(int const * const & a)

int const ia [3];

func(ia) => func<int [3]>(int const (& a) [3])

On that second example, the const appears to "move" from being applied on the array to being applied on the elements. This is because you can't really have a const array, only an array of const elements.