2

I have this working code:

template <typename X, typename Y>
auto f(X a,  Y b) { return a + b; };

template <typename X, typename Y>
auto f(X* a,  Y* b) { return *a + *b; };

int main() {
    int* p;
    int* q;
    f(p, q);
}

which compiles and runs the second f(), because it is more specialized.

However, if we const-qualify the arguments like so:

template <typename X, typename Y>
auto f(const X a, const Y b) { return a + b; };

template <typename X, typename Y>
auto f(const X* a, const Y* b) { return *a + *b; };

int main() {
    int* p;
    int* q;
    f(p, q);
}

It no longer works and tries to pick the first function, erroring out on the a + b for pointes.

Why does const-qualifying the types make the second template no longer more specialized?

Fureeish
  • 12,533
  • 4
  • 32
  • 62
  • `const X* a` is a non-`const` pointer to a `const` X. `p` and `q` both point to a non-`const` `int`. – Nathan Pierson Nov 24 '21 at 17:55
  • @NathanPierson but the second template can match only pointers, while the first one can match anything. Wouldn't that quality it to be more specialized? Is the [linked asnwer](https://stackoverflow.com/a/65230288/7151494) wrong? – Fureeish Nov 24 '21 at 17:58
  • The template is more specialized, but the first overload is being selected earlier in the process. The [best viable function](https://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function) first checks to see if one of the conversion sequences for the args is better. `int*` to `int * const` is an exact match. `int*` to `const int*` is a pointer conversion. So the first overload is selected during step 1, whereas "the second template is more specialized" only applies if they're tied as of step 5. – Nathan Pierson Nov 24 '21 at 18:05
  • Actually, sorry, I think `int*` to `const int*` is a qualification conversion, not a pointer conversion, which would make both of them an exact match. Now I'm not sure what exactly is going on. – Nathan Pierson Nov 24 '21 at 18:07
  • There might be something going on with the [thing](https://stackoverflow.com/questions/39848541/why-should-i-not-use-const-in-this-simple-function) where `const`-qualifying a value parameter doesn't really have any effect from the perspective of calling code and overload resolution, so the first overload ends up being completely identical without even needing a qualification conversion. Can't seem to find a good definitive exposition of this though. – Nathan Pierson Nov 24 '21 at 18:16
  • @NathanPierson "*`const`-qualifying a value parameter doesn't really have any effect from the perspective of calling code and overload resolution*" yeah, but if we ignore the `const`, we end up with the first snippet of code, which compiles successfully. – Fureeish Nov 24 '21 at 18:23
  • No, you don't. `X` and `const X` are the same in this context, `X*` and `const X*` are not. If you _actually_ added `const` [symmetrically](https://godbolt.org/z/M44vbjKae) you get something that does compile. – Nathan Pierson Nov 24 '21 at 18:29
  • @NathanPierson true, true. – Fureeish Nov 24 '21 at 18:38

1 Answers1

4

Why does const-qualifying the types make the second template no longer more specialized?

It doesn't.

The "problem" we're observing here is that overload resolution takes into account a few things before checking what type is more specialized.

Reading the section Best viable function we can see that:

For each pair of viable function F1 and F2, the implicit conversion sequences from the i-th argument to i-th parameter are ranked to determine which one is better (except the first argument, the implicit object argument for static member functions has no effect on the ranking)

F1 is determined to be a better function than F2 if implicit conversions for all arguments of F1 are not worse than the implicit conversions for all arguments of F2 [...]

Now let's take a look at both overloads:

template <typename X, typename Y>
auto f(const X a, const Y b) { return a + b; };

int main() {
    int* p;
    int* q;
    f(p, q);
}

Here X = int* and Y = int*. No conversions are necessary. It's worth to point out that the types of a and b are not const int*, but int* const.

Let's now take a look at the second overload:

template <typename X, typename Y>
auto f(const X* a, const Y* b) { return *a + *b; };

int main() {
    int* p;
    int* q;
    f(p, q);
}

Here, in order to get const int* (the desired type of the argument) from int* (which is the type of p and q), X and Y have to be deduced as int* and then a conversion has to occur. That's because const int* doesn't mean const pointer to int, but a pointer to const int. Pointer to const int and pointer to int are not the same, but the latter is convertible to the former. There is no way to deduce X and Y for it to be no conversions in this case.

Thus, overload resolution chooses the candidate with fewer conversions. That overload tries to add two pointers and it results in a compile-time error.

Fureeish
  • 12,533
  • 4
  • 32
  • 62
  • This is the best reason to prefer East Const (right const) as opposed to Const West.: compare `T const x` and `T const* c`. – Ben Nov 25 '21 at 02:35