Why is neither of these different user-defined conversion sequences better in overload resolution?
Asked Answered
U

3

8

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.

Usher answered 8/12, 2016 at 13:11 Comment(3)
@πάνταῥεῖ It's free function overload, no member function involved at allMayda
"Of course I could .get() the raw pointer and use it in the call but I want to eliminate the raw pointers altogether." Eleminating raw pointers is not necessarily the best course of action. In many cases, a raw pointer will be much more performant than a shared_ptr, as the container decreases the locality to memory accesses to that instance. If you have time, I'd encourage you to check out Herb Sutter's Smart Pointer Parameters post.Petersen
@Petersen You are right of course, but I need shared pointers to rescue the code from the state of chaos that was caused by many people passing many object around using raw pointers, and I am willing to pay a small price for some order. Thanks for the link, I do pass shared pointers by constant reference whenever possible.Usher
S
1

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).

Swath answered 8/12, 2016 at 13:25 Comment(2)
It works but I don't understand... We have the same two functions, plus two more. Is there a rule that says that a template function is a better match than a non-template function? Can you point to some further reading?Usher
Now I understand. Thanks!Usher
A
0

Tag dispatching can solve the issue.
It follows a minimal, working example:

#include <iostream>
#include<memory>

struct A {};

struct B : public A {};

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

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

template<typename T>
void f(std::shared_ptr<T> ptr)
{
    f(ptr.get(), ptr);
}

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

As you can see, you already have what you need to create your tag: the stored pointer.
Anyway, you don't have to get it and pass it around at the call point. Instead, by using an intermediate function template, you can use it as a type to dispatch your calls internally. You don't even have to name the parameter if you don't want to use it.

Aubree answered 8/12, 2016 at 13:40 Comment(0)
I
0

The reason for the ambiguity is that both std::shared_ptr<A> and std::shared_ptr<const A> can be constructed from std::shared_ptr<B>, due to the converting constructor template. See [util.smartptr.shared.const]:

shared_ptr(const shared_ptr& r) noexcept;
template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept;

18   Remarks: The second constructor shall not participate in overload resolution unless Y* is compatible with T*.

Thus both overloads have the exact same rank, specifically a user-defined conversion, which leads to the ambiguity during overload resolution.

As a workaround we just need an overload for non-const types:

template <class U, std::enable_if_t<!std::is_const_v<U> && std::is_convertible_v<U*, A*>, int> = 0>
void f(std::shared_ptr<U> a) {
    f(std::static_pointer_cast<A>(a));
}

With that we hide the user-defined conversion, making this overload a better match.

Intersperse answered 8/10, 2021 at 12:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.