Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overload resolution and shared pointers to const

I'm converting a large code to use custom shared pointers instead of raw pointers. I have a problem with overload resolution. Consider this example:

#include <iostream>

struct A {};

struct B : public A {};

void f(const A*)
{
    std::cout << "const version\n";
}

void f(A*)
{
    std::cout << "non-const version\n";
}

int main(int, char**)
{
    B* b;
    f(b);
}

This code correctly writes "non-const version" because qualification conversions play a role in ranking of implicit conversion sequences. Now take a look at a version using shared_ptr:

#include <iostream>
#include<memory>

struct A {};

struct B : public A {};

void f(std::shared_ptr<const A>)
{
    std::cout << "const version\n";
}

void f(std::shared_ptr<A>)
{
    std::cout << "non-const version\n";
}

int main(int, char**)
{
   std::shared_ptr<B> b;
   f(b);
}

This code doesn't compile because the function call is ambiguous.

I understand that user-defined deduction-guide would be a solution but it still doesn't exist in Visual Studio.

I'm converting the code using regexp because there are thousands of such calls. The regexps cannot distinguish calls that match the const version from those that match the non-const version. Is it possible to take a finer control over the overload resolution when using shared pointers, and avoid having to change each call manually? Of course I could .get() the raw pointer and use it in the call but I want to eliminate the raw pointers altogether.

like image 331
Dragan Vidovic Avatar asked Dec 08 '16 13:12

Dragan Vidovic


1 Answers

You could introduce additional overloads to do the delagation for you:

template <class T>
void f(std::shared_ptr<T> a)
{
  f(std::static_pointer_cast<A>(a));
}

template <class T>
void f(std::shared_ptr<const T> a)
{
  f(std::static_pointer_cast<const A>(a));
}

You can potentially also use std::enable_if to restrict the first overload to non-const Ts, and/or restrict both overloads to Ts derived from A.

How this works:

You have a std::shared_ptr<X> for some X which is neither A nor const A (it's either B or const B). Without my template overloads, the compiler has to choose to convert this std::shared_ptr<X> to either std::shared_ptr<A> or std::shared_ptr<const A>. Both are equally good conversions rank-wise (both are a user-defined conversion), so there's an ambiguity.

With the template overloads added, there are four parameter types to choose from (let's analyse the X = const B case):

  1. std::shared_ptr<A>
  2. std::shared_ptr<const A>
  3. std::shared_ptr<const B> instantiated from the first template, with T = const B.
  4. std::shared_ptr<const B> instatiated from the second template, with T = B.

Clearly types 3 and 4 are better than 1 and 2, since they require no conversion at all. One of them will therefore be chosen.

The types 3 and 4 are identical by themselves, but with overload resolution of templates, additional rules come in. Namely, a template which is "more specialised" (more of the non-template signature matches) is preferred over one less specialised. Since overload 4 had const in the non-template part of the signature (outside of T), it's more specialised and is therefore chosen.

There's no rule that says "templates are better." In fact, it's the opposite: when a template and a non-template have the same cost, the non-template is preferred. The trick here is that the template(s) have lesser cost (no conversion required) than the non-template (user-defined conversion required).

like image 105
Angew is no longer proud of SO Avatar answered Nov 20 '22 13:11

Angew is no longer proud of SO