“Transferring a person’s warmth to another’s chest”—— Eason Chan “Love Transfer”
Introduction
The updates to the C++23 language core are minimal, and I have not felt motivated to upgrade. However, considering that next year is 2026, using C++23 still seems reasonable, right? Among the features in C++23 that can significantly simplify code, the most prominent is probably deducing this. Therefore, I first used this feature to refactor the code, and finally, I no longer have to repeatedly write <span>&</span>, <span>const &</span>, <span>&&</span>, and <span>const &&</span> for member functions. With the addition of <span>forward_like</span>, a single member function can cover all scenarios. Initially, it went smoothly, but when refactoring with <span>tuple</span>, I encountered compilation errors in unit tests. Upon investigation, I found that <span>forward_like</span> is not applicable to the <span>get</span> method of <span>tuple</span>.
Problem
For <span>std::tuple</span>, the implementation intent of the <span>get</span> function is to expose the access capabilities of the members within the <span>tuple</span>. To simplify, I will use <span>struct</span> for discussion, as shown in the following code:
struct foo { int &lvref_m;};void test() { int number = 1; const auto &obj = foo{number}; obj.lvref_m = 2; // Okay!}
It can be seen that when <span>obj</span> is of type <span>const foo &</span>, the type of the member <span>lvref_m</span> that needs to be exposed should be <span>int &</span>. If we use <span>forward_like</span> to implement the <span>get</span> method, its return type will be <span>const int &</span>, because <span>forward_like</span> adds the <span>const</span> qualifier to the member type. There are many other scenarios where it is not applicable, which I will not elaborate on here. Here is the implementation of cppreference‘s <span>forward_like</span>:
template <class T, class U>constexpr auto&& forward_like(U&& x) noexcept { constexpr bool is_adding_const = std::is_const_v<std::remove_reference_t<T>>; if constexpr (std::is_lvalue_reference_v<T&&>) { if constexpr (is_adding_const) { return std::as_const(x); } else { return static_cast<U&>(x); } } else { if constexpr (is_adding_const) { return std::move(std::as_const(x)); } else { return std::move(x); } }}
This implementation is quite intuitive, which led me to wonder if <span>tuple</span> could only be handled specially? Upon closer inspection, I found the following description on cppreference:
The main scenario that std::forward_like caters to is adapting “far” objects. Neither the tuple nor the language scenarios do the right thing for that main use-case, so the merge model is used for std::forward_like.
It turns out I was too focused on the code!<span>std::forward_like</span> is indeed not applicable to <span>tuple</span>; this is just an implementation suitable for the main scenario. Proposal P2445 mentions three models: <span>merge</span>, <span>tuple</span>, and <span>language</span>, while <span>std::forward_like</span> uses the <span>merge</span> model. The specific differences between these three models are summarized in the following table from the proposal:
| Owner | Member | ‘merge’ | ‘tuple’ | ‘language’ |
|---|---|---|---|---|
<span>&</span> |
<span>&&</span> |
<span>&</span> |
<span>&</span> |
|
<span>&&</span> |
<span>&</span> |
<span>&&</span> |
<span>&</span> |
<span>&</span> |
<span>const</span> |
<span>&</span> |
<span>const &&</span> |
<span>&</span> |
<span>&</span> |
<span>const &</span> |
<span>&</span> |
<span>const &</span> |
<span>&</span> |
<span>&</span> |
<span>const &&</span> |
<span>&</span> |
<span>const &&</span> |
<span>&</span> |
<span>&</span> |
<span>&&</span> |
<span>&&</span> |
<span>&&</span> |
<span>&</span> |
<span>&</span> |
<span>&&</span> |
<span>&&</span> |
<span>&&</span> |
<span>&&</span> |
<span>&</span> |
<span>const</span> |
<span>&&</span> |
<span>const &&</span> |
<span>&&</span> |
<span>&</span> |
<span>const &</span> |
<span>&&</span> |
<span>const &</span> |
<span>&</span> |
<span>&</span> |
<span>const &&</span> |
<span>&&</span> |
<span>const &&</span> |
<span>&&</span> |
<span>&</span> |
<span>const &</span> |
<span>const &&</span> |
<span>const &</span> |
<span>const &</span> |
|
<span>&&</span> |
<span>const &</span> |
<span>const &&</span> |
<span>const &</span> |
<span>const &</span> |
<span>const</span> |
<span>const &</span> |
<span>const &&</span> |
<span>const &</span> |
<span>const &</span> |
<span>const &&</span> |
<span>const &</span> |
<span>const &&</span> |
<span>const &</span> |
<span>const &</span> |
<span>const &&</span> |
<span>const &&</span> |
<span>const &&</span> |
<span>const &</span> |
|
<span>&&</span> |
<span>const &&</span> |
<span>const &&</span> |
<span>const &&</span> |
<span>const &</span> |
<span>const</span> |
<span>const &&</span> |
<span>const &&</span> |
<span>const &&</span> |
<span>const &</span> |
Implementation
For the <span>tuple</span> model, the Owner and Member value categories need to be collapsed, while the <span>const</span> qualifier is inherited from the member. I must say, when I first saw the original code provided in the proposal, I doubted whether this was a C++23 proposal. However, considering it is all for the love of coding, the flaws do not overshadow the merits! Here is a simplified implementation of <span>forward_like_tuple</span>:
template <typename T, typename U>constexpr auto &&forward_like_tuple(auto &&u) noexcept { constexpr auto is_const_this = std::is_const_v<std::remove_reference_t<T>>; if constexpr (std::is_lvalue_reference_v<T>) { if constexpr (is_const_this) { return static_cast<const U &>(u); } else { return static_cast<U &>(u); } } else { if constexpr (is_const_this) { return static_cast<const U &&>(u); } else { return static_cast<U &&>(u); } }}
In practice, I did not use the four-branch version provided above, but rather a simplified version with three branches. However, to align with the proposal and avoid misleading, I will not include it here. Of course, the above code is still not rigorous and lacks constraints. The deduced type of <span>u</span> and the template type <span>U</span> need to be similar, meaning that after removing <span>cvref</span>, they should be of the same type, defined as follows:
template <typename T, typename U>concept is_similar = std::is_same_v<std::remove_cvref_t<T>, std::remove_cvref_t<U>>;
The reason I did not add constraints directly is that its usage scenario is very narrow, and users should be clear about what they are doing. Moreover, the constraints need to use traits like <span>std::remove_cvref_t</span>, which is implemented using template classes. For the compiler, instantiating template classes is heavier than function overloading. While using deducing this simplifies the code, it is also crucial to avoid increasing compilation time.
Testing
The proposal has provided test code, avoiding redundant work. This test code includes <span>std::tuple</span> as a control test. After extracting and modifying, the complete test code can be found on compiler explorer (https://godbolt.org/z/86e4an4ne), with part of the code shown below:
template <typename T, typename U>using _copy_ref_t = std::conditional_t< std::is_rvalue_reference_v<T>, U &&, std::conditional_t<std::is_lvalue_reference_v<T>, U &, U>>;template <typename T, typename U>using _copy_const_t = std::conditional_t< std::is_const_v<std::remove_reference_t<T>>, _copy_ref_t<U, std::remove_reference_t<U> const>, U>;template <typename T, typename U>using _copy_cvref_t = _copy_ref_t<T &&, _copy_const_t<T, U>>;struct probe { };template <typename M>struct S { M m; using value_type = M;};template <typename T, typename, typename Tuple, typename>void test() noexcept { using value_type = typename std::remove_cvref_t<T>::value_type; using tpl_model = decltype(std::get<0>(std::declval<_copy_cvref_t<T, std::tuple<value_type>>>())); using tpl = decltype(forward_like_tuple<T, value_type>(std::declval<value_type>())); static_assert(std::is_same_v<Tuple, tpl>); // sanity checks static_assert(std::is_same_v<Tuple, tpl_model>);}void test() noexcept { using p = probe; // clang-format off // TEST TYPE ,'merge' ,'tuple' ,'language' test<S<p > , p && , p && , p && >(); test<S<p > & , p & , p & , p & >(); test<S<p > && , p && , p && , p && >(); test<S<p > const , p const &&, p const &&, p const &&>(); test<S<p > const & , p const & , p const & , p const & >(); test<S<p > const &&, p const &&, p const &&, p const &&>(); test<S<p const > , p const &&, p const &&, p const &&>(); test<S<p const > & , p const & , p const & , p const & >(); test<S<p const > && , p const &&, p const &&, p const &&>(); test<S<p const > const , p const &&, p const &&, p const &&>(); test<S<p const > const & , p const & , p const & , p const & >(); test<S<p const > const &&, p const &&, p const &&, p const &&>(); test<S<p & > & , p & , p & , p & >(); test<S<p && > & , p & , p & , p & >(); test<S<p const & > & , p const & , p const & , p const & >(); test<S<p const &&> & , p const & , p const & , p const & >(); test<S<p const & > const & , p const & , p const & , p const & >(); test<S<p const &&> const & , p const & , p const & , p const & >(); test<S<p & > , p && , p & , p & >(); test<S<p & > && , p && , p & , p & >(); test<S<p & > const , p const &&, p & , p & >(); test<S<p & > const & , p const & , p & , p & >(); test<S<p & > const &&, p const &&, p & , p & >(); test<S<p && > , p && , p && , p & >(); test<S<p && > && , p && , p && , p & >(); test<S<p && > const , p const &&, p && , p & >(); test<S<p && > const & , p const & , p & , p & >(); test<S<p && > const &&, p const &&, p && , p & >(); test<S<p const & > , p const &&, p const & , p const & >(); test<S<p const & > && , p const &&, p const & , p const & >(); test<S<p const & > const , p const &&, p const & , p const & >(); test<S<p const & > const &&, p const &&, p const & , p const & >(); test<S<p const &&> , p const &&, p const &&, p const & >(); test<S<p const &&> && , p const &&, p const &&, p const & >(); test<S<p const &&> const , p const &&, p const &&, p const & >(); test<S<p const &&> const &&, p const &&, p const &&, p const & >(); // clang-format on}
Conclusion
<span>tuple</span>‘s <span>get</span> method appears to be significantly simplified after using deducing this and <span>forward_like_tuple</span>. However, from the compiler’s perspective, this merely transforms several overloaded <span>get</span> method functions into several compile-time branches within <span>forward_like_tuple</span>. The same applies to <span>forward_like</span>, and comparing the implementations of <span>forward_like</span> and <span>forward_like_tuple</span>, both can structurally correspond. This article is just a preliminary discussion; proposal P2445 provides a more detailed description.
Finally, here is a little poemReturning from playing ball at nightby Qi YuThe bright moon in the sky follows the colorful clouds, returning with excitement, my steps are light.On a hot summer night, why worry about the heat? My travel clothes are soaked, let the wind blow.