C++ Overload Resolution

C++ overload resolution

The two “overloads” aren't in the same scope. By default, the compiler only considers the smallest possible name scope until it finds a name match. Argument matching is done afterwards. In your case this means that the compiler sees B::DoSomething. It then tries to match the argument list, which fails.

One solution would be to pull down the overload from A into B's scope:

class B : public A {
public:
using A::DoSomething;
// …
}

How does overload resolution work when an argument is an overloaded function?

Let's take the most interesting case,

bar("abc", foo);

The primary question to figure out is, which overload of bar to use. As always, we first get a set of overloads by name lookup, then do template type deduction for each function template in the overload set, then do overload resolution.

The really interesting part here is the template type deduction for the declaration

template<class T> void bar(T* x, void (*f)(T*)) {}

The Standard has this to say in 14.8.2.1/6:

When P is a function type, pointer to function type, or pointer to member function type:

  • If the argument is an overload set containing one or more function templates, the parameter is treated as a non-deduced context.

  • If the argument is an overload set (not containing function templates), trial argument deduction is attempted using each of the members of the set. If deduction succeeds for only one of the overload set members, that member is used as the argument value for the deduction. If deduction succeeds for more than one member of the overload set the parameter is treated as a non-deduced context.

(P has already been defined as the function template's function parameter type including template parameters, so here P is void (*)(T*).)

So since foo is an overload set containing a function template, foo and void (*f)(T*) don't play a role in template type deduction. That leaves parameter T* x and argument "abc" with type const char[4]. T* not being a reference, the array type decays to a pointer type const char* and we find that T is const char.

Now we have overload resolution with these candidates:

void bar(int x, void (*f)(int)) {}                             // (1)
void bar(double x, void (*f)(double)) {} // (2)
void bar(std::string x, void (*f)(std::string)) {} // (3)
void bar<const char>(const char* x, void (*f)(const char*)) {} // (4)
void bar(A x, void (*f2)(double)) {} // (5)

Time to find out which of these are viable functions. (1), (2), and (5) are not viable because there is no conversion from const char[4] to int, double, or A. For (3) and (4) we need to figure out if foo is a valid second argument. In Standard section 13.4/1-6:

A use of an overloaded function name without arguments is resolved in certain contexts to a function, a pointer to function or a pointer to member function for a specific function from the overload set. A function template name is considered to name a set of overloaded functions in such contexts. The function selected is the one whose type is identical to the function type of the target type required in the context. The target can be

  • ...
  • a parameter of a function (5.2.2),
  • ...

... If the name is a function template, template argument deduction is done (14.8.2.2), and if the argument deduction succeeds, the resulting template argument list is used to generate a single function template specialization, which is added to the set of overloaded functions considered. ...

[Note: If f() and g() are both overloaded functions, the cross product of possibilities must be considered to resolve f(&g), or the equivalent expression f(g). - end note]

For overload (3) of bar, we first attempt type deduction for

template<class T> void foo(T* ) {}

with target type void (*)(std::string). This fails since std::string cannot match T*. But we find one overload of foo which has the exact type void (std::string), so it wins for the overload (3) case, and overload (3) is viable.

For overload (4) of bar, we first attempt type deduction for the same function template foo, this time with target type void (*)(const char*) This time type deduction succeeds, with T = const char. None of the other overloads of foo have the exact type void (const char*), so the function template specialization is used, and overload (4) is viable.

Finally, we compare overloads (3) and (4) by ordinary overload resolution. In both cases, the conversion of argument foo to a pointer to function is an Exact Match, so neither implicit conversion sequence is better than the other. But the standard conversion from const char[4] to const char* is better than the user-defined conversion sequence from const char[4] to std::string. So overload (4) of bar is the best viable function (and it uses void foo<const char>(const char*) as its argument).

Using concepts for function overload resolution (instead of SFINAE)

Yes concepts are designed for this purpose. If a sent parameter doesn't meet the required concept argument the function would not be considered in the overload resolution list, thus avoiding ambiguity.

Moreover, if a sent parameter meets several functions, the more specific one would be selected.

Simple example:

void print(auto t) {
std::cout << t << std::endl;
}

void print(std::integral auto i) {
std::cout << "integral: " << i << std::endl;
}

Above print functions are a valid overloading that can live together.

  • If we send a non integral type it will pick the first
  • If we send an integral type it will prefer the second

e.g., calling the functions:

print("hello"); // calls print(auto)
print(7); // calls print(std::integral auto)

No ambiguity -- the two functions can perfectly live together, side-by-side.

No need for any SFINAE code, such as enable_if -- it is applied already (hidden very nicely).


Picking between two concepts

The example above presents how the compiler prefers constrained type (std::integral auto) over an unconstrained type (just auto). But the rules also apply to two competing concepts. The compiler should pick the more specific one, if one is more specific. Of course if both concepts are met and none of them is more specific this will result with ambiguity.

Well, what makes a concept be more specific? if it is based on the other one1.

The generic concept - GenericTwople:

template<class P>
concept GenericTwople = requires(P p) {
requires std::tuple_size<P>::value == 2;
std::get<0>(p);
std::get<1>(p);
};

The more specific concept - Twople:

class Any;

template<class Me, class TestAgainst>
concept type_matches =
std::same_as<TestAgainst, Any> ||
std::same_as<Me, TestAgainst> ||
std::derived_from<Me, TestAgainst>;

template<class P, class First, class Second>
concept Twople =
GenericTwople<P> && // <= note this line
type_matches<std::tuple_element_t<0, P>, First> &&
type_matches<std::tuple_element_t<1, P>, Second>;

Note that Twople is required to meet GenericTwople requirements, thus it is more specific.

If you replace in our Twople the line:

    GenericTwople<P> && // <= note this line

with the actual requirements that this line brings, Twople would still have the same requirements but it will no longer be more specific than GenericTwople. This, along with code reuse of course, is why we prefer to define Twople based on GenericTwople.


Now we can play with all sort of overloads:

void print(auto t) {
cout << t << endl;
}

void print(const GenericTwople auto& p) {
cout << "GenericTwople: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}

void print(const Twople<int, int> auto& p) {
cout << "{int, int}: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}

And call it with:

print(std::tuple{1, 2});        // goes to print(Twople<int, int>)
print(std::tuple{1, "two"}); // goes to print(GenericTwople)
print(std::pair{"three", 4}); // goes to print(GenericTwople)
print(std::array{5, 6}); // goes to print(Twople<int, int>)
print("hello"); // goes to print(auto)

We can go further, as the Twople concept presented above works also with polymorphism:

struct A{
virtual ~A() = default;
virtual std::ostream& print(std::ostream& out = std::cout) const {
return out << "A";
}
friend std::ostream& operator<<(std::ostream& out, const A& a) {
return a.print(out);
}
};

struct B: A{
std::ostream& print(std::ostream& out = std::cout) const override {
return out << "B";
}
};

add the following overload:

void print(const Twople<A, A> auto& p) {
cout << "{A, A}: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}

and call it (while all the other overloads are still present) with:

    print(std::pair{B{}, A{}}); // calls the specific print(Twople<A, A>)

Code: https://godbolt.org/z/3-O1Gz


Unfortunately C++20 doesn't allow concept specialization, otherwise we would go even further, with:

template<class P>
concept Twople<P, Any, Any> = GenericTwople<P>;

Which could add a nice possible answer to this SO question, however concept specialization is not allowed.


1 The actual rules for Partial Ordering of Constraints are more complicated, see: cppreference / C++20 spec.

Overload resolution between constructors and conversion operators

Why A's constructor is the better choice in the first case? B's conversion operator seems to be the better match since it doesn't require an implicit conversion from B<int> to A<int>.

I believe this choice is due to the open standard issue report CWG 2327:

2327. Copy elision for direct-initialization with a conversion function

Section: 11.6 [dcl.init]

Status: drafting

Submitter: Richard Smith

Date: 2016-09-30

Consider an example like:

struct Cat {};
struct Dog { operator Cat(); };

Dog d;
Cat c(d);

This goes to 11.6 [dcl.init] bullet 17.6.2: [...]

Overload resolution selects the move constructor of Cat. Initializing the Cat&& parameter of the constructor results in a temporary, per 11.6.3 [dcl.init.ref] bullet 5.2.1.2. This precludes the possitiblity of copy elision for this case.

This seems to be an oversight in the wording change for guaranteed copy elision. We should presumably be simultaneously considering both constructors and conversion functions in this case, as we would for copy-initialization, but we'll need to make sure that doesn't introduce any novel problems or ambiguities..

We may note that it both GCC and Clang picks the conversion operator (even though the issue is not yet a resolved DR) from versions 7.1 and 6.0, respectively (for C++17 language level); prior to these releases both GCC and Clang chose the A<X>::A(A<U> &&) [T = X, U = int] ctor overload.

Why the first and second cases yield different results? What has changed in C++17?

C++17 introduced guaranteed copy elision, meaning the compiler must omit the copy and move construction of class objects (even if they have side effects) under certain circumstances; if the argument in the issue above hold, this is such a circumstance.


Notably, GCC and Clang both lists unknown (/or none) status of CWG 2327; possibly as the issue is it still in status Drafting.



C++17: guaranteed copy/move elision & aggregate initialization of user-declared constructors

The following program is well-formed in C++17:

struct A {                                                                               
A() = delete;
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
};

struct B {
B() = delete;
B(const B&) = delete;
B(B&&) = delete;
B& operator=(const B&) = delete;
B& operator=(B&&) = delete;

operator A() { return {}; }
};

int main ()
{
//A a; // error; default initialization (deleted ctor)
A a{}; // OK before C++20: aggregate initialization

// OK int C++17 but not C++20:
// guaranteed copy/move elision using aggr. initialization
// in user defined B to A conversion function.
A a1 (B{});
}

which may come as a surprise. The core rule here is that both A and B are aggregates (and may thus be initialized by means of aggregate initialization) as they do not contain user-provided constructors, only (explicitly-deleted) user-declared ones.

C++20 guaranteed copy/move elision & stricter rules for aggregate initialization

As of P1008R1, which has been adopted for C++20, the snippet above is ill-formed, as A and B are no longer aggregates as they have user-declared ctors; prior to P1008R1 the requirement were weaker, and only for types not to have user-provided ctors.

If we declare A and B to have explicitly-defaulted definitions, the program is naturally well-formed.

struct A {                                                                               
A() = default;
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
};

struct B {
B() = default;
B(const B&) = delete;
B(B&&) = delete;
B& operator=(const B&) = delete;
B& operator=(B&&) = delete;

operator A() { return {}; }
};

int main ()
{
// OK: guaranteed copy/move elision.
A a1 (B{});
}

C++ Overload resolution with universal reference function template which can't be changed

Update for C++20: The answer below remains true for C++11 through C++17, but in C++20 you can do this:

template <typename T>
requires std::same_as<std::remove_cvref_t<T>, A>
auto foo(T&& t) {
// since this is more constrained than the generic forwarding reference
// this one should be preferred for foo(A{})
}

Which you can get using more convenient syntax by creating a named concept:

template <typename T, typename U>
concept DecaysTo = std::same_as<std::decay_t<U>, T>;

// longest form
template <typename T> requires DecaysTo<T, A> void foo(T&&);

// middle form
template <DecaysTo<A> T> void foo(T&&);

// abbreviated form
void foo(DecaysTo<A> auto&&);

Honestly, I think you're out of luck here. The typical approaches all fail. You can do...

SFINAE?

template <typename T> auto foo(T&& );
template <typename T,
typename = only_if_is<T, A>>
auto foo(T&& );

foo(A{}); // error: ambiguous

Write a class that takes an l-or-rvalue reference?

template <typename T> lref_or_ref { ... };

template <typename T> auto foo(T&& );
auto foo(lref_or_ref<A> );

foo(A{}); // calls general, it's a better match

The best you could do is introduce a forwarding function using a chooser:

template <int I> struct chooser : chooser<I - 1> { };
template <> struct chooser<0> { };

template <typename T>
auto bar(T&& t, chooser<0> ) {
// worst-option, general case
foo(std::forward<T>(t));
}

template <typename T,
typename = only_if_is<T, A>>
auto bar(T&& t, chooser<1>) {
// A-specific
}

template <typename T>
auto bar(T&& t) {
bar(std::forward<T>(t), chooser<20>{});
}

But you mentioned in a comment that this doesn't work for you either.

Forcing C++ to prefer an overload with an implicit conversion over a template

If you want to change the overloads such that the former overload is chosen whenever there is an implicit conversion possible, with the latter being the backup, you can do this with SFINAE via std::enable_if:

#include <type_traits>

void call_function(const std::function<void()>& function)
{
std::cout << "CALL FUNCTION 1" << std::endl;
function();
}

template <typename Function,
// Consider this overload only if...
typename std::enable_if<
// the type cannot be converted to a std::function<void()>
!std::is_convertible<const Function&, std::function<void()>>::value,
int>::type = 0>
void call_function(const Function& function)
{
std::cout << "CALL FUNCTION 2" << std::endl;
function();
}

Demo


Alternatively, if you want to be able to support an unknown number of overloads of call_function with the "CALL FUNCTION 2" being a backup overload in case none of the functions work, you can do this too, but it requires quite a bit more work:

// Rename the functions to `call_function_impl`
void call_function_impl(const std::function<void()>& function)
{
std::cout << "CALL FUNCTION 1" << std::endl;
function();
}

void call_function_impl(const std::function<void(int, int)>& function)
{
std::cout << "CALL FUNCTION 2" << std::endl;
function(1, 2);
}

// The backup function must have a distinct name
template <typename Function>
void call_function_backup_impl(const Function& function)
{
std::cout << "CALL FUNCTION backup" << std::endl;
function();
}


// Implement std::void_t from C++17
template <typename>
struct void_impl {
using type = void;
};

template <typename T>
using void_t = typename void_impl<T>::type;

// Make a type trait to detect if the call_function_impl(...) call works
template <typename Function, typename = void>
struct has_call_function_impl
: std::false_type
{};

template <typename Function>
struct has_call_function_impl<Function,
void_t<decltype(call_function_impl(std::declval<const Function&>()))>>
: std::true_type
{};


// If the call_function_impl(...) call works, use it
template <typename Function,
typename std::enable_if<
has_call_function_impl<Function>::value,
int>::type = 0>
void call_function(const Function& function)
{
call_function_impl(function);
}

// Otherwise, fall back to the backup implementation
template <typename Function,
typename std::enable_if<
!has_call_function_impl<Function>::value,
int>::type = 0>
void call_function(const Function& function)
{
call_function_backup_impl(function);
}

Demo

How do I remove a special constructor from overload resolution?

A move constructor has one T&& argument, and possibly additional arguments provided those have default values. That means you can add std::enable_if_t<Condition, int> = 0 as an additional argument to your move constructor.

The compiler won't create a one-argument optional::optional(T&&) move constructor when you have that two-argument move constructor.



Related Topics



Leave a reply



Submit