Реализация концепции для концепции Pair with C ++ 20 - PullRequest
3 голосов
/ 23 февраля 2020

Для печати любого вида std::pair мы можем реализовать следующий метод:

template<typename First, typename Second>
void printPair(const std::pair<First, Second>& p) {
    std::cout << p.first << ", " << p.second << std::endl;
}

Но предположим, что мы хотим реализовать метод, который может печатать пару любого типа, не обязательно std::pair, на основе следующих требований:

  • имеет поля first и second publi c
  • имеет first_type и second_type publi c внутренние типы
  • тип first == first_type
  • тип second == second_type

Имея concept, давайте назовите это Pair , может позволить написать такой метод, как:

void printPair(const Pair auto& p) {
    std::cout << p.first << ", " << p.second << std::endl;
}

Как будет определен такой concept?

Ответы [ 4 ]

8 голосов
/ 23 февраля 2020

Здесь есть несколько интересных тонкостей.

template<class P>
concept Pair = requires(P p) {
    typename P::first_type;
    typename P::second_type;
    p.first;
    p.second;
    requires std::same_as<decltype(p.first), typename P::first_type>;
    requires std::same_as<decltype(p.second), typename P::second_type>;
};

Первые четыре строки несколько избыточны, но могут помочь получить более качественные сообщения об ошибках. Остальные строки должны быть понятны. Обратите внимание, что использование decltype при доступе к элементу простого класса приводит к объявленному типу элемента данных.

Последние две строки также можно записать как

    { p.first } -> std::same_as<typename P::first_type&>;
    { p.second } -> std::same_as<typename P::second_type&>;

Здесь составное требование применяет ограничение типа к decltype((p.first)). Это выражение является lvalue, поэтому созданный тип является ссылочным типом lvalue. Обратите внимание, что эта версия будет принимать как first_type first;, так и first_type& first;.

3 голосов
/ 23 февраля 2020

После комментария @Nicol Bolas к исходному вопросу, я согласен, что сужение concept для разрешения только пары, которая соответствует std::pair требованиям, не является лучшим дизайном, было бы лучше разрешить все следующее:

  • std::pair и аналогичные классы с полями first и second
  • std::tuple размера 2, std::array размера 2 и аналогичные классы

Это правда, что std::pair попадает в обе категории, так как он предлагает синтаксис, подобный кортежу, однако мы будем sh, чтобы иметь возможность приспосабливать типы пользователей, которые предоставляют first и second полей, но не реализуют синтаксис, подобный кортежу.

Для этого мы можем реализовать два отдельных понятия, а затем использовать соединение для создания третьего:


1. SimplePair concept

template<class P>
concept SimplePair = requires(P p) {
    p.first;
    p.second;
};

2. TuplePair concept

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

^ и выше поддерживает также std :: array


3. Пара concept

template<class P>
concept Pair = TuplePair<P> || SimplePair<P>;

Теперь у нас может быть общая c printPair , используя requires clause внутри if constexpr:

void printPair(const Pair auto& p) {
    if constexpr( SimplePair<decltype(p)> ) {
        std::cout << p.first << ", " << p.second << std::endl;
    }
    else {
        std::cout << std::get<0>(p) << ", " << std::get<1>(p) << std::endl;
    }
}

пример использования

struct MyPair {
    int first = 5;
    const char* second = "six";
};

int main() {
    printPair(std::make_tuple(1, "two")); // 1, two
    printPair(std::make_pair(3, 4));      // 3, 4
    printPair(MyPair{});                  // 5, six
    printPair(std::array{7, 8});          // 7, 8
    // not allowed, compilation error:
    // printPair(std::array{9, 10, 11});
    // printPair(std::make_tuple("one"));
    // printPair(std::make_tuple(1, 2, 3));
}

Код: https://godbolt.org/z/MXgqu3

1 голос
/ 23 февраля 2020

Старый синтаксис - для исторических целей

Приведенный ниже код был действителен в определенный момент времени с ранними версиями Concepts Technical Specification и компилируется с экспериментальным реализация, но была изменена в более поздних версиях TS и больше не действует с C ++ 20 spe c. Он хранится здесь по историческим причинам и в качестве примечания об изменении в спецификации c.


Старая версия Concepts TS имела следующий синтаксис:

template<typename _pair>
concept Pair = requires(_pair p) {
    { p.first } -> typename _pair::first_type;
    { p.second } -> typename _pair::second_type;
};

Выше синтаксис недопустим в C ++ 20. Действительный синтаксис C ++ 20 см. В других ответах на этот вопрос.


Это позволит обобщенной c printPair работать для std::pair, а также для любой другой пользовательской "пары" соответствует требованиям пары:

void printPair(const Pair auto& p) {
    std::cout << p.first << ", " << p.second << std::endl;
}

struct UserPair {
    int first = 1;
    const char* second = "hello";
    using first_type = decltype(first);
    using second_type = decltype(second);
};

int main() {
    printPair(std::make_pair(1, 3));
    printPair(UserPair{});
}

Пример рабочего кода с старой версией TS : https://godbolt.org/z/x6f76D

0 голосов
/ 11 мая 2020

Мне очень понравился этот вопрос и обсуждения вокруг него, особенно решение от T. C (у меня нет 50 комментариев, поэтому я буду публиковать комментарии в качестве другого решения). Я только что перешел из аналогичной ситуации, когда нужна пара, но также нужна библиотека для работы на C ++ 17 и C ++ 20.

Это решение от T. C. работает как для c ++ 17, так и для c ++ 20.

template<class P>
concept bool Pair = requires(P p) {
    typename P::first_type;
    typename P::second_type;
    p.first;
    p.second;
    requires my_same_as<decltype(p.first), typename P::first_type>;
    requires my_same_as<decltype(p.second), typename P::second_type>;
};

, где my_same_as определяется как std::same_as из c ++ 20:

template<class Me, class Other>
  concept bool my_same_as = std::is_same_v<Me, Other> && std::is_same_v<Other, Me>;

Имею пробовал несколько «реализаций пар», и интересным моментом является то, что поля first и second могут отличаться от ссылочных или нереферентных типов.

T. C. упомянул, что мы могли бы заменить поля на:

    { p.first } -> my_same_as<typename P::first_type&>;
    { p.second } -> my_same_as<typename P::second_type&>;

Я обнаружил, что это работает только на c ++ 20, как ни странно, не на c ++ 17 (прекрасно компилируется, но не соответствует концепции!) , Каким-то образом он не соответствует ни ссылочным, ни ссылочным (требуется сложная реализация с || и std::remove_reference_t).

Одно портативное решение, которое я нашел для c ++ 17 и c ++ 20 было:

template<typename P>
concept bool Pair = requires(P p)
{   
    typename P::first_type;
    typename P::second_type;
    { p.first } -> my_convertible_to<typename P::first_type>;
    { p.second } -> my_convertible_to<typename P::second_type>;
};

, где my_convertible_to эквивалентно std::convertible_to из c ++ 20:

template <class From, class To>
concept bool my_convertible_to =
  std::is_convertible_v<From, To> &&
  requires(std::add_rvalue_reference_t<From> (&f)()) {
    static_cast<To>(f());
  };

Я не могу объяснить, почему это тонкое поведение изменилось с c ++ 17 на c ++ 20 (на is_same_v logi c), но я публикую здесь, так как это может помочь другим в аналогичной ситуации. Я использовал g ++ - 8 для c ++ 17 и g ++ - 10.1 для c ++ 20. Спасибо за все обучение!

...