十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

effective modern C++- item 1: 理解模版类型推导

effective modern C++- item 1: 理解模版类型推导 一、问题的核心模型先记住这个框架后面所有讨论都套用它templatetypename T void f(ParamType param); // ParamType 是 T 加上某种修饰如 T、T、const T、T、T* f(expr); // 编译器从 expr 推导 T 和 ParamType关键点编译器要推导两个东西 ——T和ParamType而且它们往往不一样。很多人以为T就是expr的类型这是最大的误解。ParamType的形状是引用、指针还是按值决定了推导规则。一共分三种情况。二、Case 1ParamType 是引用或指针但不是万能引用规则两步如果expr是引用先忽略引用部分然后拿expr的类型去和ParamType做模式匹配推出T。templatetypename T void f(T param); int x 27; const int cx x; const int rx x; f(x); // T int, param 的类型 int f(cx); // T const int, param 的类型 const int f(rx); // T const int, param 的类型 const int 引用性被丢掉const 保留 的话也是注意const被保留了。这符合直觉你传一个 const 对象给一个T编译器必须让param也是 const否则你就能通过param修改一个常量。如果参数是const T那 const 就成了ParamType的一部分T里就不再需要 consttemplatetypename T void f(const T param); f(x); // T int, param const int f(cx); // T int, param const int f(rx); // T int, param const int指针的情况完全类似不展开。指针int* const的话会变成int去掉const,因为这个是不修改指针所以没必要保留int就是int对低延迟的意义const T是你在热路径里传大对象的默认武器 ——零拷贝。它反对的旧写法是按值传大对象void process(std::vectordouble data); // 旧每次调用都拷贝整个 vector堆分配 memcpy void process(const std::vectordouble data); // 新传一个指针大小的引用0 拷贝在低延迟场景一次意外的vector拷贝可能就是几百纳秒到几微秒的堆分配 缓存污染。理解 Case 1 让你清楚只要参数写成引用推导就不会引入拷贝。三、Case 2ParamType 是万能引用Universal / Forwarding ReferenceT这是全书最重要的机制之一完美转发Item 24、25的根基。templatetypename T void f(T param); int x 27; const int cx x; f(x); // x 是左值 → T int, param int f(cx); // cx 是左值 → T const int, param const int f(27); // 27 是右值 → T int, param int规则传左值→T被推导成左值引用这是 C 里唯一T会被推成引用的情况传右值→ 按 Case 1 的普通规则走T是非引用类型。背后的机制是引用折叠reference collapsing当T int时T就是int 折叠成int。这套机制让同一个T参数既能绑定左值又能绑定右值并且记住了实参的左值/右值属性从而配合std::forward实现完美转发。对低延迟的意义万能引用 std::forward是既不多拷贝、也不多移动的关键。它反对的旧写法是为左值和右值各写一个重载或统一按值收然后 move// 旧写法 A重载爆炸N 个参数要 2^N 个重载 void emplace(const Widget w); void emplace(Widget w); // 新写法一个模板搞定实参是什么值类别就原样转发过去 templatetypename T void emplace(T w) { storage.push_back(std::forwardT(w)); // 左值→拷贝右值→移动精确匹配 }std::vector::emplace_back、std::make_unique都靠这个。在热路径构造对象时它能避免先构造临时对象再拷进容器的额外一次构造析构。四、Case 3ParamType 按值传递T paramtemplatetypename T void f(T param); // param 是 expr 的一份拷贝规则三步如果expr是引用忽略引用忽略const忽略volatile。int x 27; const int cx x; const int rx x; f(x); // T int, param int f(cx); // T int, param int ← const 被丢掉 f(rx); // T int, param int为什么丢掉 const因为param是一份独立的拷贝改动它不影响原对象所以让它可修改是安全且合理的。一个关键陷阱低延迟相关只有顶层 const被丢掉指向物的 const 不会丢templatetypename T void f(T param); const char* const ptr hello; // ptr 本身是 const顶层丢掉指向的 char 是 const底层保留 f(ptr); // T const char* —— 你得到一个可以改指向、但不能改内容的指针拷贝对低延迟的意义Case 3 的价值在于告诉你按值到底拷贝了什么。对于小而廉价可拷贝的类型int、double、指针、std::string_view、小的 POD struct按值传是最优的 —— 它们在寄存器里传递没有间接寻址比引用还快且对编译器更友好无别名 aliasing利于优化。它反对的是什么都用const的教条void tick(const int price); // 反模式为一个 int 引入一层间接、可能阻碍寄存器优化 void tick(int price); // 更好int 直接进寄存器经验法则sizeof ≤ 2 个指针、且拷贝廉价的类型按值传大对象或拷贝昂贵的用const或万能引用。五、数组的退化decay这块对写低延迟栈上缓冲区的人特别有用。按值时数组退化成指针丢失长度信息templatetypename T void f(T param); const char name[] J. P. Briggs; // 类型是 const char[13] f(name); // T const char* ← 数组退化成指针13 这个长度没了按引用时数组类型含长度被完整保留templatetypename T void f(T param); f(name); // T const char[13], param const char()[13]这个特性可以在编译期拿到数组长度零运行时开销templatetypename T, std::size_t N constexpr std::size_t arraySize(T ()[N]) noexcept { return N; // 编译期常量 } int vals[] {1, 2, 3, 4, 5}; int buffer[arraySize(vals)]; // buffer 大小编译期确定纯栈分配0 堆开销对低延迟的意义它反对旧 C 写法传数组 单独传一个size_t len参数既啰嗦又容易传错、还是运行期变量。用引用绑定数组长度是编译期N可以用来在栈上开固定缓冲、展开循环、让编译器做边界优化全程不碰堆。函数类型也有完全对应的退化规则按值退化成函数指针按引用保留。六、函数类型的退化function-to-pointer decay函数类型也能被推导规则和数组一模一样按值 / 按普通引用会退化成函数指针按引用T保留原始函数类型。void someFunc(int, double); // 函数类型是 void(int, double) templatetypename T void f1(T param); // 按值 templatetypename T void f2(T param); // 按引用 f1(someFunc); // T void(*)(int, double) ← 退化成函数指针 f2(someFunc); // T void()(int, double) ← 保留成函数引用对照记忆数组 vs 函数完全平行实参T param按值T param按引用数组const char[13]const char*退化成指针const char()[13]保留长度函数void(int,double)void(*)(int,double)退化成函数指针void()(int,double)保留函数类型为什么会退化因为 C 继承自 C 的规则函数名和数组名在大多数表达式里会隐式转成指针这叫 decay。当参数按值接收时你不可能拷贝一个函数或拷贝一整个数组的实体进去所以语言就退化成传一个指针。只有绑引用时不需要拷贝才能保住原始类型。对低延迟的意义这块直接关系到你怎么传可调用对象callback / 比较器 / 处理函数而传法不同能不能内联、有没有间接跳转差别巨大// 反模式函数退化成函数指针传进去 void run(void (*cb)(Event)); // cb 是运行期指针 → 每次调用是一次间接跳转编译器通常无法内联 // 低延迟首选用模板 万能引用接可调用对象保留其具体类型 templatetypename F void run(F cb) { // F 可以是 lambda 类型、函数对象类型 cb(evt); // 类型已知 → 编译器可完全内联0 间接跳转 }关键点函数指针会丢掉具体是哪个函数这个编译期信息退化成一个运行期地址热路径里每次调用都要通过这个地址间接跳转还常常阻断内联和后续优化。而如果你用模板参数F直接绑 lambda / 函数对象F是一个独一无二的具体类型编译器看得见函数体能把整个调用内联展开、和调用点一起做优化。这就是为什么std::sort传 lambda 比 C 的qsort吃函数指针快好几倍——本质就是这里的退化规则在起作用。所以低延迟里的实践取向是别让可调用对象退化成函数指针用模板/万能引用把它的具体类型带进来。std::function也有类似的类型擦除 间接调用开销热路径同样要慎用。七、总结与实战决策表你的意图参数写法推导结果低延迟影响只读大对象const T保留 const0 拷贝首选无拷贝只读小对象 (int/指针/string_view)T按值剥掉顶层 const进寄存器最快需要转发/通用绑定T左值→T右值→T配合 forward0 冗余拷贝/移动需要修改实参T保留 const无拷贝需要数组长度T ()[N]保留长度 N编译期尺寸栈分配这个 Item 帮你建立的核心直觉参数的形状值/引用/万能引用在编译期就决定了会不会拷贝、会不会退化、const 会不会保留。低延迟工程的本质之一就是在编译期消灭一切不必要的拷贝和堆分配而这一切都始于你能一眼看穿一次函数调用背后编译器到底推导出了什么、生成了几次拷贝。
返回列表