forward_like_tuple

「把一个人的温暖转移到另一个的胸膛」
——陈奕迅《爱情转移》

前言

C++23语核更新太少,一直没有动力去升级,不过考虑到明年是2026年了,26年用C++23还算合理吧?C++23中,能大幅简化代码的特性,首当其冲的恐怕就是deducing this了,于是先使用该特性对代码进行了一番改造,终于不用再反复写&const &&&const &&这几种成员函数了,搭配上forward_like,一个成员函数即覆盖所有情形,起初还算顺利,但是改造在tuple时却碰到了单元测试编译报错的问题,一探之下,发现forward_like并不适用于tupleget方法。

问题

对于std::tupleget函数的实现意图是为了将tuple中的成员的访问能力暴露出来,为了简化,这里用struct代替讨论,代码如下:

1
2
3
4
5
6
7
8
struct foo {
    int &lvref_m;
};
void test() {
    int number = 1;
    const auto &obj = foo{number};
    obj.lvref_m = 2; // Okay!
}

可以看出,当objconst foo &类型时,需要暴露的lvref_m成员的类型应该是int &,如果使用forward_like实现get方法,其返回类型将是const int &,因为forward_like会把const限定符加到成员类型上,还有许多其他情形也不适用,这里不再赘述。摘取cppreferenceforward_like实现,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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);
        }
    }
}

这个实现很符合直觉,于是我心里却冒出疑问,对于tuple只能特殊处理了吗?细看之下才发现cppreference有如下描述:

1
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.

原来是我光顾着看代码了!std::forward_like确实不适用于tuple,这只是一个适用主要场景的实现,提案P2445提到了3种模型分别为mergetuplelanguage,而std::forward_like使用的是merge模型。这3种模型的具体区别,摘取提案表格如下:

Owner Member ‘merge’ ’tuple' ’language'
& && & &
&& & && & &
const & const && & &
const & & const & & &
const && & const && & &
&& && && &
&& && && && &
const && const && && &
const & && const & & &
const && && const && && &
const & const && const & const &
&& const & const && const & const &
const const & const && const & const &
const && const & const && const & const &
const && const && const && const &
&& const && const && const && const &
const const && const && const && const &
const && const && const && const && const &

实现

对于tuple模型,OwnerMember值类别需要折叠,而const限定符则从成员继承。不得不说,最初看到提案给的原始代码时,我都怀疑这是一个C++23的提案,但考虑到都是为爱发电,瑕不掩瑜吧!这里给出一个更现代且更简单的forward_like_tuple的实现,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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);
        }
    }
}

在实践时,笔者并没有使用上面给出的4个分支的版本,而是使用的3个分支的简化版本,但为契合提案和避免误导,就不必放出来了。当然,以上代码还不严谨,缺乏约束,u的推导类型和模板类型U需要相似,即,二者去除cvref之后是相同类型,定义如下:

1
2
template <typename T, typename U>
concept is_similar = std::is_same_v<std::remove_cvref_t<T>, std::remove_cvref_t<U>>;

之所以没有直接加上约束,是因为其使用场景很窄,使用者应当清楚自己在做什么,而且约束中需要使用std::remove_cvref_t这样的trait,这种trait是使用模板类实现的,对于编译器来说,模板类实例化是要比函数重载更重的,使用deducing this简化代码的同时,尽量避免增加编译时间也至关重要。

测试

提案已经给了测试代码,避免了重复劳动,该测试代码包括了std::tuple作为对照测试,截取并删改后,完整测试代码见compiler explorer,部分代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
}

总结

tupleget 方法在使用deducing thisforward_like_tuple之后看上去是简化不少,可从编译器的视角来看,这只是把几个get方法函数重载变成了forward_like_tuple内部的若干个编译期分支,forward_like也是如此,而且对比forward_likeforward_like_tuple的代码实现,二者从结构上也能形成对应。本文只为抛砖,提案 P2445有更为详尽的描述。

0%