万能引用进行重载的替代方案:标签派发的疑问? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX    C++

万能引用进行重载的替代方案:标签派发的疑问?

  •  
  •   amiwrong123 2022-01-27 18:50:48 +08:00 2301 次点击
    这是一个创建于 1419 天前的主题,其中的信息可能已经有所发展或是发生改变。

    执行 logAndAdd(1)后,调用流程比较有意思:

    • logAndAdd 函数,实参是 1
    • logndAddImpl 的 true_type 版本
    • logAndAdd 函数,实参是一个 string
    • logAndAddImpl 的 false_type 版本

    为什么最后一个函数不直接这么写呢?

    void logAndAddImpl(int idx, std::true_type) { logAndAddImpl(nameFromIdx(idx), std::false_type); } 

    这样调用流程还能少一次呢。

    来自 effective modern c++,条款 26 。疑问来自 179 页最下面。

    第 1 条附言    2022-01-27 21:06:12 +08:00

    借自己楼问一下: 这种 <typename T, typename = xxx> 的typename = xxx是什么意思啊,给我个关键词,我可以自己去百度。

    第 2 条附言    2022-01-27 21:27:26 +08:00

    由于我还没来得及看 模型编程相关知识,只能从我目前的知识来理解一下:

    模板参数也是参数,只是它不像普通形参是实参传递给它的。而模型参数,尤其这里的模板函数里的模板参数T,那都是推断出来的,而 <typename T, typename = xxx>后面的部分,我理解是就是 另一个模板参数,但是这个参数没有名字(函数声明也可以这样声明:void fun(int) )。typename = xxx后面是一个std::enable_if,std::enable_if会返回一个类型,或者返回一个void类型。

    • 返回void类型,就相当于这个模板参数不成立。这个模板函数也就不能调用
    • 返回一个类型,是一个有效的模板参数。这个模板函数能调用
    11 条回复    2022-02-04 04:22:17 +08:00
    GeruzoniAnsasu
        1
    GeruzoniAnsasu  
       2022-01-28 06:10:51 +08:00   1
    原则:面向接口编程
    有个隐含约定即,任何一个「实现」都不应该默认理解「另外一个实现」

    即使不看这个例子,我举一个抽象的:
    有个 dispatch 表,满足某些条件会先进行预处理
    预处理完了之后你想把数据 re-dispatch ,此时你会
    * 重新调用 dispatch 的接口?
    * 还是直接调用「你已经知道的那种情况的」具体流程?

    --------

    template <typename T, typename=std::enable_if_t<「对 T 的判断」>>
    是一个经典的 SFINAE 惯用法: https://cpppatterns.com/patterns/class-template-sfinae.html
    当 enable_if 的判断条件不成立,enable_if::type 就不存在,因此外面的模板 typename=enable_if<>::type 这里就不能推导出正确的类型,这个模板偏特化就会被跳过

    还有一种 SFINAE 的方式是利用函数重载
    https://cpppatterns.com/#/search/SFINAE

    不过在今天已经基本可以用 constexpr if 取代,属于时代遗珍了
    dangyuluo
        2
    dangyuluo  
       2022-01-28 08:18:37 +08:00   2
    哥们,从你这些天的发帖可以看出来,你是有进行思考的。子曰:学而不思则罔,思而不学则殆。我觉得你应该沉下心来多读一些资料,从基础开始打起,而不是遇到一个问题就发帖。就算别人给你解释了,可能也不是你能理解的,反而会令你更加混乱。
    dangyuluo
        3
    dangyuluo  
       2022-01-28 08:26:20 +08:00   1
    至于你的问题,你完全可以在`logAndAddImpl(int, std::true_type)`里调用`logAndAddImpl(std::string, std::false_type)`。Scott Meyers 没这么写只是因为他单纯没这么写。如果他写了`logAndAddImpl(std::string, std::false_type)`,你可能又会来问他为什么不调用`logAndAdd`让函数自己推导模板了。

    另外相信编译器,O3 一开这种简单级别的函数调用都会被优化掉的。
    amiwrong123
        4
    amiwrong123  
    OP
    &nsp;  2022-01-28 13:15:31 +08:00
    @dangyuluo #2
    谢谢哥们提醒,这两天发帖确实有点心急了。其实发帖前,我也是尽量去看书了(手头目前有这几本 c++经典书籍,c++ primer ,effctive c++, effctive c++more, 深入探索 c++对象模型,effective modern c++,c++编程思想。编程思想 900 多页,哎,我留到最后慢慢看吧),还有看网上的资料,比如有 www.cplusplus.comhttps://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md https://www.cprogramming.com/tutorial/lesson1.html ,还有网上的一大堆博客。( cplusplus 是个好资料,但对于现在的我来说,只适合浅尝辄止)( PS:资料太多感觉自己要迷失在知识的海洋里了)

    类似“知识的诅咒”,有时候感觉自己会受到“没有知识的诅咒”,就是一个问题摆在我面前,我甚至都不知道该去搜素什么关键词来解答自己的问题(比如复制消除、rule of five 。当然,就算按照“定义了移动构造函数,会导致赋值操作符被删除”来搜索应该也能搜到 rule of five ,但过程可能会很曲折,当然这可能也和搜索技巧有关)。反正这种就很尴尬,当然这也与我 资料看得太少有关。

    总之,我会尽量多查阅资料后再发帖的。
    dangyuluo
        5
    dangyuluo  
       2022-01-28 13:58:09 +08:00
    你是靠着兴趣来学习 C++,想转方向么?还是说有项目驱动。
    amiwrong123
        6
    amiwrong123  
    OP
       2022-01-28 14:58:37 +08:00
    @dangyuluo #5
    目前没有项目驱动。第一份工作是干的 Android framework 开发,但安卓没学到太多,c++当时也没好好学。现在第二份工作基本没用到 c++(也不是用的 java ,反正工作内容不太喜欢),最近想跳槽。
    之前花了很多功夫研究 java ,想转 java 方向,但没成功(也不算白学,安卓 framework 开发也会写 java )。
    最近终于下定决心 走 Android framework 开发或者 c++开发了,这两个都对 c++有要求。个人有一定 c++基础,但感觉知识很不系统,所以最近在好好看一遍。
    FrankHB
        7
    FrankHB  
       2022-01-30 04:21:54 +08:00   1
    @GeruzoniAnsasu 这个隐含约定在这里不适用。这是给接口的用户看的,而这里实现和实现之间的关系是用户不应该可见的实现细节,所以其实无所谓。
    甚至一般有经验的实现者会推荐 OP 的写法。因为这种写法最小化了内部实现和公共接口的依赖,维护者更容易划分出哪些(连续的)代码完全是实现细节,在一定程度上提高了实现内部的模块化,增加了可修改性。

    内部实现故意去调用公开接口增加调用层次是不寻常的,特别是行为可能有差异。如:

    class B
    {
    public: void f(){f_impl();}
    private: virtual void f_impl(){/*...*/};
    public:
    void g1(){f();}
    void g2(){f_impl();}
    void g3(){B::f_impl();}
    };

    像 f_impl 就算是 private 也允许 override ,所以不是绝对意义上的内部实现。
    而这里 g1 g2 g3 的含义就是不一样的。什么时候用什么得看你接口设计是拿来干什么的,而不是有一个教条。
    在 B 的实现内部,如果硬要说考虑 f 或者 f_impl 应该怎么调用,那么尽量用 B::f_impl ,因为行为最确定;其次如果有要求允许 B 外的 overrider 就用 f_impl ,至少在 B 这个类内仍同属不可被类外访问的实现细节;最次才是用 f ,表示不同寻常的“就需要依赖外部接口”(此时通常还需要实现的注释)。
    B 外要调用,当然是直接 f 了。如果需要在 B 外部 B::f_impl 或者 f_impl ,那这个就不该 private (名字也不该叫 _impl ),不过一般相当罕见。

    你所谓的“重新调用 dispatch 的接口”还是“直接调用「你已经知道的那种情况的」具体流程”也应该看设计明确要支持的需求。如果设计不明就会出岔子。甚至极端点说,re-dispatching 这种形式本身就可能是可疑的伪需求。GCC 和 Clang 对 ELF symbol interposition 默认处理的不同就是这类岔子的一个现实例子,这时候就要扯皮了: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100937
    FrankHB
        8
    FrankHB  
       2022-01-30 04:43:33 +08:00   1
    <typename T, typename = xxx> 单独没什么实际意思,就 typename 后的模板形式参数允许省略没用到的名称这个知识点。
    大部分情况下,看到不懂的构造,前后的标识符都可以当作关键字。这里如果要搜,直接搜 enable_if 就可以(另外有 C++14 简写 enable_if_t );事实上 enable_if/enable_if_t 原则上就只有一种用法,就是通过 SFINAE 选择需要的 overload/specialization 。

    这样也更容易给 enable_if 出现在其它地方的各种奇葩用法打预防针(随便抄几坨我实际写过的):

    template<class _tRange, yimpl(typename... _tParams,
    typename = enable_if_t<sizeof...(_tParams) == 0>)>
    auto
    begin(_tRange& c, yimpl(_tParams&&...)) -> decltyp(c.begin())
    {
    return c.begin();
    }

    template<typename _tFrom, typename _tTo, typename _type = void>
    using enable_if_convertible_t = enable_if_t<is_convertible<_tFrom, _tTo>::value, _type>;

    template<typename _fCallable, typename _type, typename... _tParams>
    auto
    invoke_impl(_fCallable&& f, _type&& obj, _tParams&&... args)
    -> enable_if_t<is_callable_case1<decay_t<_fCallable>, _type>::value,
    decltype((yforward(obj).*f)(yforward(args)...))>
    {
    return yconstraint(f), (yforward(obj).*f)(yforward(args)...);
    }

    template<typename _type, enable_if_t<!is_floating_point<_type>::value, int> = 0>
    inline bool
    Do(const _type& x)
    {
    return x % _type(2) != _type(0);
    }

    template<typename _type>
    struct is_string_like<_type, enable_if_t<
    is_object<decay_t<decltype(std::declval<_type>()[0])>>::value>> : true_
    {};

    以上只是展示 enable_if 能出现在什么地方,省略了一些为什么需要这么写的上下文(比如其它 overload 和 primary template ),所有共通用法都是 SFINAE (虽然混了点 expression SFINAE )。当然高玩可能可以在缺失上下文的情况下大致上猜出为什么需要(不得不)这样写,并累积对 C++ 的仇恨值而加快升级。
    FrankHB
        9
    FrankHB  
       2022-01-30 04:56:58 +08:00   1
    @GeruzoniAnsasu 另外我还是得提一下,那些 std::true_type 之类的重载 C++17 以来确实可以用 if constexpr 替代,但 OP 那个借楼问 enable_if 例子还就不行(甚至是个 C++20 concept 也不好用的地方),因为是哪来防止隐式上下文里非预期的 overload 的,而不是你自己能手动决定什么时候能加 if constpexr 的地方。如果不踢掉这个 overload ,该传值的转移或者复制构造的调用会匹配到构造模板上。

    这是个著名的烂坑,因为太常见(基本上只要任何写单参数构造模板的地方都会坑)我是简写了:
    https://github.com/FrankHB/YSLib/blob/master/YBase/include/ystdex/meta.hpp#L948
    (实现本身也算是个 enable_if 的应用举例。)
    FrankHB
        10
    FrankHB  
       2022-01-30 05:16:23 +08:00   1
    @dangyuluo “相信编译器”是说给那些怀疑自己比编译器聪明但实际上没有什么基础(甚至连生成的代码都不知道怎么看)的用户听的,不适用于 OP 。
    对 OP 的情况,该强调的是“不要做过早的优化”,但是判断什么算过早,根本还是在用户自己。

    对性能要求极端一些的情况下,其实主流编译器真不那么靠谱;甚至可以反过来说如果要求确实很高时,一些关键的上下文怎么都不该相信编译器(事实上,这种必要场合并不多,开发者应避免任意诉诸审查编译器生成代码为导向进行优化的倾向)。
    举个例子,如果嵌套层次多的话(取决于调用上下文和具体编译器版本乃至 cc1plus 之类的参数),启发式内联是可能会睁眼瞎的,于是明明这种简单的情况真不给你内联……
    再举个例子,至少 GCC (-O3 -flto )到现在都不大能现实地保证 const allocator_type& 参数能优化得跟 allocator_type 传值质量一样高,也许这就是 C++ 没 restrict 就得老实躺平的宿命了(最蛋疼的是标准库都是钦定 const allocator_type& 的,要风格一致嘛)……
    GCC __attribute__((__always_inline__)) 可能生成错误的代码;
    作为常识,内联不是越多越好,__attribute__((__flatten__)) 可能是神器也可能是狗屎(另外 GCC 和 Clang 实现不大一样,后者是嵌套 always_inline 偷懒了),有时候反而 __attribute__((__noline__)) 才是救星……

    当然以上属实比较极端了,应用开发者一般当作过眼云烟,折腾 framework 也最好别上头。
    c0xt30a
        11
    c0xt30a  
       2022-02-04 04:22:17 +08:00
    这个例子在我看来是已经过时的。楼主似乎在读一本很老的 C++ 教科书?现在比较方便的写法是

    ```
    template<typename T>
    void func( T&& v )
    {
    if constexpr( std::is_interger_v<std::remove_cv_t<T>> )
    {
    // branch 1
    }
    else
    {
    // branch 2
    }
    }
    ```

    建议碰到有 enable_if_t 的代码略过,concept 和 constexpr-if 的引入使得 SFINAE 已经不是那么时髦(过时)了,时间有限的话没有必要深究。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4375 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 10:02 PVG 18:02 LAX 02:02 JFK 05:02
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86