Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using concepts for function overload resolution (instead of SFINAE)

Trying to say goodbye to SFINAE.

Is it possible to use concepts to distinguish between functions, so the compiler can match the correct function based on whether or not a sent parameter meets concept constraints?

For example, overloading these two:

// (a)
void doSomething(auto t) { /* */ }

// (b)
void doSomething(ConceptA auto t) { /* */ }

So when called the compiler would match the correct function per each call:

doSomething(param_doesnt_adhere_to_ConceptA); // calls (a)
doSomething(param_adheres_to_ConceptA); // calls (b)

Related question: Will Concepts replace SFINAE?

like image 213
Amir Kirsh Avatar asked Feb 26 '20 12:02

Amir Kirsh


People also ask

Do concepts replace Sfinae?

So the simple answer is YES.

What are the requirements to overload a function?

The only other requirement for successfully overloading functions is that each overloaded function must have a unique parameter list. The function return types may be the same or they may be different, but even if the return types are different, the functions must still have distinct parameter lists.

Is it possible to overload a function?

Function overloading is a feature of object-oriented programming where two or more functions can have the same name but different parameters. When a function name is overloaded with different jobs it is called Function Overloading.


1 Answers

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.

like image 57
Amir Kirsh Avatar answered Oct 20 '22 10:10

Amir Kirsh