Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template picks const reference over const pointer

Consider the following:

template <class T> void Foo(const T* x) {
  std::cout << "I am the pointer overload" << std::endl;
}

template <class T> void Foo(const T& x) {
  std::cout << "I am the reference overload" << std::endl;
}

Given the above, I would expect the following to call the pointer overload:

int* x;
Foo(x);

but it doesn't. That seems odd to me as a const T* can clearly bind to a non-const T just as well as a const T& can but the pointer variant seems like a "better fit".

For my application I want the pointer variant to be called. I can make that work via an additional specialization:

template <class T> void Foo(T* x) {
  const T* const_x = x;
  Foo(const_x);
}

but that feels wrong and unnecessary. Is there a better way? What am I not understanding (other than section x.y.z of the standard says it's this way)?

like image 505
Oliver Dain Avatar asked Dec 27 '17 07:12

Oliver Dain


2 Answers

You are thinking: If my parameter would not be constant (or if the argument would be constant) then the pointer would be a perfect match. That is correct. The difference in what I have is the const. So just adding const to the above scenario I should get the pointer overload as well. That is also correct. Now how can that be since you clearly get the reference overload? Well, the code doesn't correspond to your thought. Here is the code that would go along with your line of thinking and that would indeed select the pointer overload:

template <class T> void Foo(T* const x);    
template <class T> void Foo(T const &x);

Pay close attention to the place of const. I have added const as a top level qualifier. While T const &x is equivalent with what you have const T& x, T* const x is not the same as const T* x.

Lets see the overload resolution in this case:

 fun                                     | T    | parameter    | argument
-----------------------------------------+------+--------------+-----------
 template <class T> void Foo(T* const x) | int  | int* const   | int*
 template <class T> void Foo(T const &x) | int* | int* const & | int*

Lets see the overload with your version:

 fun                                     | T    | parameter    | argument
-----------------------------------------+------+--------------+-----------
 template <class T> void Foo(const T* x) | int  | const int*   | int*
 template <class T> void Foo(T const &x) | int* | int* const & | int*

As you can see in the first version just adding a top level const is preferred and the pointer overload is chosen. There is no pointer conversion required.

In the second case the pointer overload would require a pointer conversion from pointer to mutable to pointer to const. Those are different types of pointers. But with the 2nd overload there is no pointer conversion required. Just adding a top level const.

This is in a nutshell the best I can explain without going that's what x.y.z section of the standard says

like image 103
bolov Avatar answered Oct 07 '22 01:10

bolov


Your problem is that when compiling Foo(x); a template type deduction will be executed, and as your x is an int* this will be interpreted first as a Foo<int*>(x) call and your template <class T> void Foo(const T& x) overload is a perfect match for this, so type deduction ends here.

If you would deal with classes (instead of template functions) you could use partial template specializations to distinguish pointer-types and non-pointer-types but for functions only full specializations are allowed.

What you can do is to use SFINAE techniques like:

template <class T> void Foo(const T* x) {
    std::cout << "I am the pointer overload" << std::endl;
}

template <class T> 
typename std::enable_if<!std::is_pointer<T>::value>::type 
Foo(const T& x) {
    std::cout << "I am the reference overload" << std::endl;
}

In this case the reference overload will not match (when deducting types) if T is a a pointer-type, so the compiler will try Foo<int>(x) as well and your template <class T> void Foo(const T* x) overload will be a perfect match (just what you have expected).

sample:

int x = 12;
int* pX = new int(5);

Foo(x);
Foo(pX);

output:

I am the reference overload
I am the pointer overload
like image 25
QwerJoe Avatar answered Oct 06 '22 23:10

QwerJoe