Document #: | D1500R0 |
Date: | 2019-08-02 |
Project: | Programming Language C++ EWGI |
Reply-to: |
Michael Park <mcypark@gmail.com> Barry Revzin <barry.revzin@gmail.com> |
There are many places today where we want to deduce a specific type, but with unknown cv-ref qualifiers and unknown potentially derived type. Specific examples might be: get<>
for std::tuple
, which is currently specified as four overloads of the form:
template <size_t I, typename... Ts>
constexpr tuple_element_t<I, tuple<Ts...>>& get(tuple<Ts...>&);
template <size_t I, typename... Ts>
constexpr tuple_element_t<I, tuple<Ts...>> const& get(tuple<Ts...> const&);
template <size_t I, typename... Ts>
constexpr tuple_element_t<I, tuple<Ts...>>&& get(tuple<Ts...>&&);
template <size_t I, typename... Ts>
constexpr tuple_element_t<I, tuple<Ts...>> const&& get(tuple<Ts...> const&&);
These overloads are all fundamentally the same kind of thing, but we have to write it four times in order to ensure that we handle all four cv-ref cases as well any potentially derived type. Ideally, what we really want to write is a function template that deduces a particular type or class template with unknown qualifiers. That is, not an arbitrary forwarding reference - but a forwarding reference to a specific type.
This problem becomes much more apparent with the hopeful adoption of [P0847R2], where we run into the problem of needing to do mitigation against shadowing:
struct B {
int i;
auto&& get(this auto&& self) {
return FWD(self).i;
}
};
struct D : B {
double i;
};
D{}.get(); // oops: rvalue reference to D::i instead of B::i
What we really want B::get
to do is to take, specifically, some kind of reference to B
. We want to deduce just the qualifiers, and apply them to the member as expected, but we have no way in the language to do that today. We can’t use Concepts - those cannot perform conversions, specifically derived-to-base conversions. We can only verify if either the type is a specific kind or derives from a specific kind, but that’s not sufficient for these purposes.
To demonstrate:
void foo(Same<B> auto x);
void bar(DerivedFrom<B> auto y);
foo(D{}); // error
bar(D{}); // ok, but y is still a D when we really want it to be a B
We need to not just constrain the deduction, but also convert down to the base class for these cases.
We propose the following syntax:
to mean an overload set whose parameter is a “forwarding reference to std::string
.” This has the effect of stamping out the four functions:
void foo(std::string&);
void foo(std::string const&);
void foo(std::string&&);
void foo(std::string const&&);
This syntax is derived from the terse concepts syntax [P1141R2], where we already have as precedent the notion of abbreviated function template using the syntax Concept auto
- here instead of a concept name we use a type name. The notable difference is that we do not propose a long form syntax here - solely the terse syntax.
The model we’re proposing for this feature is stamping out the combinatorial explosion of cv-ref qualifiers (all four of them!) - which has the effect of allowing conversions in non-template scenarios.
For example:
struct X {
operator std::string() cosnt;
};
void bar(std::string auto&&);
/* roughly equivalent to
void bar(std::string&); // #1
void bar(std::string const&); // #2
void bar(std::string&&); // #3
void bar(std::string const&&); // #4
*/
bar(X{}); // ok, calls #3, bar<std::string>
But if the proposed forwarding reference function template is still an underlying template, conversion functions would not apply:
struct Y {
operator std::tuple<int>() const;
};
template <typename... Ts>
void quux(std::tuple<Ts...> auto&&);
/* roughly equivalent to
template <typename... Ts> void quux(std::tuple<Ts...>&);
template <typename... Ts> void quux(std::tuple<Ts...> const&);
template <typename... Ts> void quux(std::tuple<Ts...>&&);
template <typename... Ts> void quux(std::tuple<Ts...> const&&);
*/
quux(Y{}); // error
Even though we use the auto
keyword to declare these functions - we don’t view these kinds of functions as actual templates, behaviorally. We very much consider the right model for this proposal to be stamping out the right overloads. With the Concepts terse syntax, we are deducing an unknowably many and potentially infinite amount of types. With this proposal, we have a known, finite collection of overloads. There are only eight possibilities: the Cartesian product of {const
, non-const
} x {volatile
, non-volatile
} x { &
, &&
}. And let’s be serious, we don’t really care about volatile
.
As such, these do not have to be templates. We know at the point of declaration all the potential “instantiations” of these functions. This allows us to actually write code like:
// Person.h
class Person {
std::string last;
std::string first;
public:
Person(std::string auto&&, std::string auto&&);
};
and then define that constructor in a source file:
// Person.cxx
Person::Person(std::string auto&& l, std::string auto&& f)
: last(std::forward<decltype(l)>(l))
, first(std::forward<decltype(f)>(f))
{ }
Since these forwarding functions are not templates, they can count as special member functions. Meaning that:
Defaults both the copy and move constructors - but for both the const
and non-const
cases. This has the added benefit that such a construction avoids the usual issues with forward references - since we’re declaring the non- const
verisons as well:
struct S {
S();
S(S auto&&); // #1
template <typename T>
S(T&&); // #2
};
S s;
s s2 = s; // ok: calls #1, not #2
[P0847R2] Gašper Ažman, Simon Brand, Ben Deane, Barry Revzin. 2019. Deducing this.
https://wg21.link/p0847r2
[P1141R2] Ville Voutilainen, Thomas Köppe, Andrew Sutton, Herb Sutter, Gabriel Dos Reis, Bjarne Stroustrup, Jason Merrill, Hubert Tong, Eric Niebler, Casey Carter, Tom Honermann, Erich Keane, Walter E. Brown, Michael Spertus, Richard Smith. 2018. Yet another approach for constrained declarations.
https://wg21.link/p1141r2