Использование концепций для разрешения перегрузки функций (вместо SFINAE) - PullRequest
1 голос
/ 26 февраля 2020

Попытка попрощаться с SFINAE.

Можно ли использовать concepts для различения guish между функциями, чтобы компилятор мог сопоставить правильную функцию в зависимости от того, отправлено или нет параметр соответствует concept ограничениям?

Например, перегрузка этих двух:

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

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

Поэтому при вызове компилятор будет соответствовать правильной функции для каждого вызова:

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

Смежный вопрос: Будут ли концепции заменить SFINAE?

1 Ответ

6 голосов
/ 26 февраля 2020

Да concepts предназначены для этой цели. Если отправленный параметр не соответствует требуемому концептуальному аргументу, функция не будет учитываться в списке разрешений перегрузки, что позволяет избежать неоднозначности.

Более того, если отправленный параметр соответствует нескольким функциям, тем больше специфика c будет выбран один.

Простой пример:

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

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

Выше print функции являются допустимой перегрузкой, которая может существовать вместе.

  • Если мы отправим нецелый тип, он выберет первый
  • Если мы отправим целочисленный тип, он предпочтет второй

, например, вызов функции:

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

Нет двусмысленности - две функции могут прекрасно жить вместе, бок о бок.

Нет необходимости в каком-либо коде SFINAE , например enable_if - он уже применяется (очень хорошо спрятан).


Выбор между двумя концептами

В приведенном выше примере показано, как компилятор предпочитает тип с ограничениями ( std: : встроенное авто ) по типу без ограничений ( просто авто ). Но правила также применяются к двум конкурирующим понятиям. Компилятор должен выбрать более конкретный c, если он более конкретный c. Конечно, если оба понятия соблюдаются и ни одно из них не является более конкретным c, это приведет к неоднозначности.

Что же делает концепцию более конкретным c? если он основан на другом 1 .

Концепция generi c - GenericTwople :

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

Чем больше специфика c 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>;

Обратите внимание, что Twople требуется для удовлетворения требований GenericTwople, поэтому он более конкретен c.

Если вы замените в нашем Twople строку:

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

с учетом фактических требований, предъявляемых к этой строке, Twople будет по-прежнему иметь те же требования, но более не будет более точным c, чем GenericTwople. Это, конечно же, наряду с повторным использованием кода, поэтому мы предпочитаем определять Twople на основе GenericTwople.


Теперь мы можем играть со всеми видами перегрузок:

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;
}

И вызывать это с:

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)

Мы можем go дальше, поскольку представленная выше концепция Twople также работает с полиморфизмом:

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";
    }
};

добавить следующую перегрузку:

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

и вызовите его (пока присутствуют все другие перегрузки) с помощью:

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

Код: https://godbolt.org/z/3-O1Gz


К сожалению C + +20 не допускает специализацию концепта, в противном случае мы бы go пошли еще дальше:

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

Что могло бы добавить хороший возможный ответ на этот вопрос SO , однако специализация концепта не допускается.


1 Фактические правила для Частичное упорядочение ограничений более сложные, см .: cppreference / C ++ 20 spe c.

...