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

2022-01-27 18:50:48 +08:00
 amiwrong123

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

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

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

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

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

1961 次点击
所在节点    C++
11 条回复
GeruzoniAnsasu
2022-01-28 06:10:51 +08:00
原则:面向接口编程
有个隐含约定即,任何一个「实现」都不应该默认理解「另外一个实现」

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

另外相信编译器,O3 一开这种简单级别的函数调用都会被优化掉的。
amiwrong123
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
2022-01-28 13:58:09 +08:00
你是靠着兴趣来学习 C++,想转方向么?还是说有项目驱动。
amiwrong123
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
2022-01-30 04:21:54 +08:00
@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
2022-01-30 04:43:33 +08:00
<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
2022-01-30 04:56:58 +08:00
@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
2022-01-30 05:16:23 +08:00
@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
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 已经不是那么时髦(过时)了,时间有限的话没有必要深究。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/830993

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX