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:
first
and second
public fieldsfirst_type
and second_type
public inner typesfirst
== first_type
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 classesIt 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:
concept
template<class P>
concept SimplePair = requires(P p) {
p.first;
p.second;
};
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
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;
}
}
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));
}
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.