What is the purpose of C++20 std::common_reference?
Asked Answered
D

1

53

C++20 introduces std::common_reference. What is its purpose? Can someone give an example of using it?

Dissentient answered 23/11, 2019 at 19:23 Comment(0)
E
74

common_reference came out of my efforts to come up with a conceptualization of STL's iterators that accommodates proxy iterators.

In the STL, iterators have two associated types of particular interest: reference and value_type. The former is the return type of the iterator's operator*, and the value_type is the (non-const, non-reference) type of the elements of the sequence.

Generic algorithms often have a need to do things like this:

value_type tmp = *it;

... so we know that there must be some relationship between these two types. For non-proxy iterators the relationship is simple: reference is always value_type, optionally const and reference qualified. Early attempts at defining the InputIterator concept required that the expression *it was convertible to const value_type &, and for most interesting iterators that is sufficient.

I wanted iterators in C++20 to be more powerful than this. For example, consider the needs of a zip_iterator that iterates two sequences in lock-step. When you dereference a zip_iterator, you get a temporary pair of the two iterators' reference types. So, zip'ing a vector<int> and a vector<double> would have these associated types:

zip iterator's reference : pair<int &, double &>
zip iterator's value_type: pair<int, double>

As you can see, these two types are not related to each other simply by adding top-level cv- and ref qualification. And yet letting the two types be arbitrarily different feels wrong. Clearly there is some relationship here. But what is the relationship, and what can generic algorithms that operate on iterators safely assume about the two types?

The answer in C++20 is that for any valid iterator type, proxy or not, the types reference && and value_type & share a common reference. In other words, for some iterator it there is some type CR which makes the following well-formed:

void foo(CR) // CR is the common reference for iterator I
{}

void algo( I it, iter_value_t<I> val )
{
  foo(val); // OK, lvalue to value_type convertible to CR
  foo(*it); // OK, reference convertible to CR
}

CR is the common reference. All algorithms can rely on the fact that this type exists, and can use std::common_reference to compute it.

So, that is the role that common_reference plays in the STL in C++20. Generally, unless you are writing generic algorithms or proxy iterators, you can safely ignore it. It's there under the covers ensuring that your iterators are meeting their contractual obligations.


EDIT: The OP also asked for an example. This is a little contrived, but imagine it's C++20 and you are given a random-access range r of type R about which you know nothing, and you want to sort the range.

Further imagine that for some reason, you want to use a monomorphic comparison function, like std::less<T>. (Maybe you've type-erased the range, and you need to also type-erase the comparison function and pass it through a virtual? Again, a stretch.) What should T be in std::less<T>? For that you would use common_reference, or the helper iter_common_reference_t which is implemented in terms of it.

using CR = std::iter_common_reference_t<std::ranges::iterator_t<R>>;
std::ranges::sort(r, std::less<CR>{});

That is guaranteed to work, even if range r has proxy iterators.

Elle answered 25/11, 2019 at 1:51 Comment(8)
Maybe I'm dense, but can you clarify what the common reference is in the zip-pair example?Cacophony
Ideally, pair<T&,U&> and pair<T,U>& would have a common reference, and it would be simply pair<T&,U&>. However, for std::pair, there is no conversion from pair<T,U>& to pair<T&,U&> even though such a conversion is sound in principle. (This, incidentally, is why we don't have a zip view in C++20.)Elle
@EricNiebler: "This, incidentally, is why we don't have a zip view in C++20." Is there some reason why a zip iterator would have to use pair, instead of a type that could be specifically designed for its purpose, with appropriate implicit conversions as needed?Abernon
@Nicol Bolas There is no need to use std::pair; any suitable pair-like type with the appropriate conversions will do, and range-v3 defines such a pair-like type. On the Committee, LEWG didn't like the idea of adding to the Standard Library a type that was almost but not quite std::pair, be it normative or not, without first doing due diligence about the pros and cons of simply making std::pair work.Elle
Wouldn't it be better to use std::tuple anyway? I'm not aware of any place where a pair is better than a tuple of size 2, though they are both prevalent in the standard and other code.Nipissing
tuple, pair, tomato, to-MAH-to. pair has this nice feature that you can access the elements with .first and .second. Structured bindings help with some of the awkwardness of working with tuples, but not all.Elle
@EricNiebler So should I overload the unary operator& of the value type for the common_reference to work if I'm writing a proxy iterator? Is there an example?Elm
Don't overload unary operator& for any reason whatsoever. A simple My First Proxy Iterator example doesn't have to be complex, but all the examples I find online are.Elle

© 2022 - 2024 — McMap. All rights reserved.