V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  C++

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

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

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

    • logAndAdd 函数,实参是 1
    • logAndAddImpl 的 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
       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&&...)) -> decltype(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   ·   我们的愿景   ·   实用小工具   ·   1001 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 19:19 · PVG 03:19 · LAX 12:19 · JFK 15:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.