简体   繁体   中英

Implementing a concept for Pair with C++20 concepts

To print any kind of std::pair we can implement the following method:

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

But suppose we want to implement a method that can print a pair of any kind, not necessarily std::pair , based on the following requirements:

  • it has a first and second public fields
  • it has a first_type and second_type public inner types
  • type of first == first_type
  • type of second == second_type

Having a concept , let's call it Pair , could allow writing a method like:

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

How would such a concept be defined?

There are some interesting subtleties here.

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

The first four lines are somewhat redundant, but can help produce better error messages. The remaining lines should be self-explanatory. Note that the use of decltype on a plain class member access produces the declared type of the data member.

The last two lines could also be written as

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

Here, the compound-requirement applies the type-constraint to decltype((p.first)) . That expression is an lvalue, so the produced type is an lvalue reference type. Note that this version would accept both first_type first; and first_type& first; .

Following @Nicol Bolas comment on the original question I agree that narrowing the concept to allow only Pair that conforms to std::pair requirements is not the best design, it would be better to allow all the following:

  • std::pair and similar classes with first and second fields
  • std::tuple of size 2, std::array of size 2 and similar classes

It is true that std::pair falls in both categories, as it proposes tuple-like syntax, however we wish to be able to accommodate user types that expose first and second fields but do not implement a tuple-like syntax.

For that we can implement two separate concepts and then use conjunction to create a third one:


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

^ above supports also std::array


3. Pair concept

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

Now we can have a generic printPair , using requires clause inside 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;
    }
}

usage example

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

Code: https://godbolt.org/z/MXgqu3

Old Syntax - for Historical Purposes

The code below was valid at a certain point of time with early versions of Concepts Technical Specification , and compiles with experimental implementation, but was changed in later versions of the TS and is no longer valid with the C++20 spec. It is kept here for historical reasons and as a note on the change in the spec.


Old version of Concepts TS had the following syntax:

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

Above syntax is not valid in C++20. For the valid C++20 syntax see the other answers for this question.


This would allow a generic printPair to work for std::pair as well as for any other user "pair" that conforms with Pair requirements:

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

A working code example with old version of the TS : https://godbolt.org/z/x6f76D

I really liked this question and the discussions around it, specially the solution from TC (I don't have 50 points to comment there, so I'll post comments as another solution). I just passed from a similar situation, where a pair concept is need, but also needed the library to work on both C++17 and C++20.

This solution from TC works for both c++17 and 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>;
};

where my_same_as is defined as std::same_as from c++20:

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

I've tried several "implementations of pairs" and interesting point is that fields first and second may vary from reference or not-reference types.

TC mentioned that we could replace fields with:

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

I found out that this only works on c++20, strangely not on c++17 (it compiles fine, but doesn't match concept!). Somehow it doesn't match either reference or non-reference (requiring complex implementation with an || and std::remove_reference_t ).

One portable solution that I found for both c++17 and c++20 was:

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

where my_convertible_to is equivalent to std::convertible_to from 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());
  };

I cannot explain why this subtle behavior changed from c++17 to c++20 (on is_same_v logic), but I'm posting here as it may help others in a similar situation. I used g++-8 for c++17 and g++-10.1 for c++20. Thanks for all the learning!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM