尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++11函数模板默认参数:原理、应用与最佳实践

C++11函数模板默认参数:原理、应用与最佳实践 1. 项目概述为什么C11的默认模板参数是个“大补丁”在C98/03时代函数模板和类模板的待遇是不平等的。类模板可以拥有默认模板参数这为设计通用容器如std::vector或策略类时提供了极大的便利你可以只指定部分类型剩下的让编译器去推导或使用默认值。但函数模板呢对不起标准明确说“不行”。这导致我们在编写泛型函数时常常需要写一堆重载或者用一些奇技淫巧来模拟默认行为代码又臭又长维护起来也头疼。C11把这个历史遗留问题给解决了它允许函数模板也拥有默认模板参数。这可不是一个简单的语法糖它从根本上改变了我们设计和组织泛型代码的方式。想象一下你写一个序列化函数希望默认使用JSON格式但保留切换为XML或Protobuf的能力或者写一个算法默认使用多线程执行但允许用户指定单线程以用于调试。在C11之前实现这些需求要么需要多个函数签名要么就得用额外的函数参数来控制不够优雅。现在你可以在模板参数列表里直接给个默认值一切都变得清晰、直观。这个特性与auto、decltype、可变参数模板等一起构成了C11现代泛型编程的基石。它让函数模板的声明更加灵活和强大接口设计可以更简洁同时保持高度的可定制性。对于日常开发中需要编写库、框架或者复杂工具函数的开发者来说这是一个必须掌握的核心特性。接下来我们就深入拆解它的用法、原理以及那些容易踩坑的细节。2. 核心语法与基本用法解析2.1 语法格式与位置C11中函数模板的默认模板参数语法与类模板非常相似直接写在模板参数声明里使用等号赋值。template typename T int, typename Container std::vectorT void myFunction(const Container c) { // 函数体 }这里我们定义了一个函数模板myFunction。它有两个模板参数T和Container。T的默认类型是intContainer的默认类型是std::vectorT。这意味着如果你调用myFunction(someVec)并且someVec是std::vectorint那么编译器会推导出T为intContainer为std::vectorint完全匹配默认参数。关键规则一默认模板参数的声明顺序与函数默认参数类似默认模板参数必须从右向左连续提供。也就是说如果一个模板参数有默认值那么它右边所有的模板参数也必须都有默认值。// 正确U有默认值它右边的W也有默认值 template typename T, typename U int, typename W double void func1(T t) {} // 错误U有默认值但它右边的T没有默认值编译报错 // template typename T int, typename U, typename W double // void func2(U u) {}这个规则是为了避免歧义。在调用func2(42)时编译器无法确定42这个int类型是用来匹配T使用默认值int还是用来匹配U推导为int。强制从右向左默认可以确保推导过程是明确、无二义的。2.2 与模板参数推导的交互这是理解默认模板参数行为的关键。函数模板的实例化是模板参数推导和默认模板参数共同作用的结果其优先级是推导 默认。编译器首先尝试从函数调用实参中推导出所有模板参数。如果某个模板参数无法推导且该参数提供了默认值则使用默认值。如果无法推导且没有默认值则编译错误。看一个例子template typename T int void print(T value) { std::cout value std::endl; } int main() { print(42); // 情况1推导成功。T被推导为int忽略默认值int。 print(3.14); // 情况2推导成功。T被推导为double忽略默认值int。 printint(42); // 情况3显式指定。T被指定为int与默认值相同。 print(); // 情况4错误无法推导T因为函数调用没有提供实参。 // 虽然T有默认值int但函数参数value缺少实参。 }print(42)能成功是因为从实参42推导出了T int。此时模板声明中的 int这个默认值根本没有出场机会。print(42)在语法上也是合法的尖括号表示一个空的模板参数列表告诉编译器“我不用显式指定类型但你也不要用默认值请全部尝试推导”这里推导出int所以也能运行。但是print()会失败因为编译器看到空的模板参数列表会尝试去推导T但函数调用没有提供任何实参推导失败。即使T有默认值int编译器在推导阶段失败后并不会“回头”去使用默认值来补全函数签名因为函数调用本身缺少value的实参就是不完整的。这引出了一个重要技巧默认模板参数主要用于为那些无法或不易从函数参数推导出的模板参数提供后备值。2.3 一个实用的基础示例让我们设计一个allocate函数它用于分配内存。我们可能希望指定元素的类型T和使用的分配器Alloc。通常用户只关心类型T分配器希望用一个合理的默认值比如std::allocator。#include memory #include vector #include iostream template typename T, typename Allocator std::allocatorT T* allocate(std::size_t n, const Allocator alloc Allocator()) { // 使用分配器alloc分配n个T类型对象的内存 // 这里简化处理直接使用allocator的allocate方法 return alloc.allocate(n); } int main() { // 只指定大小使用默认的std::allocatorint int* p1 allocateint(10); std::cout Allocated int array with default allocator. std::endl; // 指定大小和自定义分配器这里仍用默认构造的std::allocator为例 int* p2 allocateint(10, std::allocatorint()); std::cout Allocated int array with provided allocator. std::endl; // 错误示例无法推导T必须显式指定或通过其他参数推导 // int* p3 allocate(10); // 编译错误无法推导模板参数‘T’ }这个例子展示了默认模板参数的典型应用场景Allocator是一个策略类用户通常不关心其具体类型使用标准库提供的默认分配器就足够了。通过赋予它默认值std::allocatorT函数接口变得非常干净。用户只需要关注核心类型T和大小n即可。当有高级需求时又可以传入自定义的分配器类型和实例灵活性十足。注意函数模板的默认模板参数和函数的默认参数是独立的它们可以同时使用如上例中的const Allocator alloc Allocator()。这进一步增强了接口的简洁性。3. 高级应用场景与设计模式掌握了基本语法后我们来看看默认模板参数如何解决实际工程中的复杂问题以及它催生的一些新的设计模式。3.1 简化复杂函数模板的接口这是最直接的收益。假设我们有一个用于配置处理的泛型函数parseConfig它需要解析器类型Parser、输出容器类型Container并且内部使用一个线程池执行线程池的类型是ThreadPool。在C11之前用户调用这个函数可能需要写一长串模板参数// 没有默认模板参数C98/03风格 template typename Parser, typename Container, typename ThreadPool Container parseConfig(const std::string filename, Parser parser, ThreadPool pool); // 调用 MyParser parser; MyThreadPool pool; std::vectorConfigItem result parseConfigstd::vectorConfigItem, MyParser, MyThreadPool(config.xml, parser, pool);接口极其冗长而且很多类型如Container其实可以从返回值期待类型或函数参数中推导但语法上必须显式写出。在C11中我们可以为这些“策略”或“上下文”参数提供合理的默认值// C11 with default template arguments template typename T, typename Parser XmlParserT, // 默认XML解析器 typename Container std::vectorT, // 默认vector容器 typename ThreadPool StdThreadPool // 默认标准线程池 Container parseConfig(const std::string filename, Parser parser Parser(), ThreadPool pool getGlobalThreadPool()) { // 实现逻辑... Container configItems; // ... 使用parser和pool解析filename并填充configItems return configItems; } // 调用变得极其简洁 int main() { // 最常见用法只关心配置项类型其他全部用默认组件 auto config1 parseConfigConfigItem(config.xml); // 需要自定义解析器但容器和线程池用默认 auto config2 parseConfigConfigItem, JsonParserConfigItem(config.json); // 完全自定义所有组件 MyCustomParser parser; MyCustomThreadPool pool; std::listConfigItem config3 parseConfigConfigItem, MyCustomParser, std::listConfigItem, MyCustomThreadPool(config.bin, parser, pool); }通过默认模板参数最常见的调用方式简化到了一行代码。用户只需要关注最核心的模板参数这里是配置项类型T其他可定制的部分都被隐藏在了简洁的接口之后按需提供即可。这大大提升了库的易用性。3.2 结合SFINAE与标签分发实现编译期多态默认模板参数与SFINAESubstitution Failure Is Not An Error结合可以创造出非常强大的编译期选择逻辑。常见的应用是提供多个默认实现并根据类型特性选择其中一个。例如我们想实现一个advance函数对于随机访问迭代器如指针、vector::iterator使用操作对于其他迭代器使用循环。我们可以利用std::iterator_traits和默认模板参数来优雅地实现标签分发。#include iterator #include type_traits // 为随机访问迭代器准备的实现快速版本 template typename Iter void advance_impl(Iter it, typename std::iterator_traitsIter::difference_type n, std::random_access_iterator_tag) { it n; // 随机访问迭代器支持常数时间跳跃 std::cout Using random_access advance (fast). std::endl; } // 为输入/向前/双向迭代器准备的实现慢速版本 template typename Iter void advance_impl(Iter it, typename std::iterator_traitsIter::difference_type n, std::input_iterator_tag) { std::cout Using input_iterator advance (slow). std::endl; while (n 0) { it; --n; } while (n 0) { --it; n; } } // 主函数模板利用默认模板参数和类型推导选择正确的实现 template typename Iter, typename Tag typename std::iterator_traitsIter::iterator_category void my_advance(Iter it, typename std::iterator_traitsIter::difference_type n) { advance_impl(it, n, Tag{}); // 分发到对应的实现 } int main() { std::vectorint vec {1,2,3,4,5}; auto vec_it vec.begin(); my_advance(vec_it, 2); // 调用random_access版本 std::cout *vec_it std::endl; // 输出 3 std::listint lst {1,2,3,4,5}; auto lst_it lst.begin(); my_advance(lst_it, 2); // 调用input_iterator版本 std::cout *lst_it std::endl; // 输出 3 }在这个例子中my_advance的第二个模板参数Tag有一个默认值它是通过std::iterator_traits从迭代器类型Iter中提取出的“迭代器类别标签”。这个默认值至关重要它使得用户调用时完全无需关心标签的存在。编译器在实例化my_advance时会自动计算出Tag的具体类型如std::random_access_iterator_tag或std::bidirectional_iterator_tag然后将其作为第三个参数传递给advance_impl。advance_impl通过函数重载决议选择匹配标签类型的那个版本。整个过程在编译期完成零运行时开销且接口对调用者透明。3.3 作为元编程中条件编译的开关默认模板参数可以很方便地引入一个“默认策略”或“默认特性”。结合std::enable_if或C17的if constexpr可以实现复杂的条件编译。假设我们有一个泛型的serialize函数对于算术类型直接进行二进制拷贝对于其他类型则调用其serialize成员函数。我们可以使用一个默认的“序列化策略”模板参数来实现。#include type_traits #include iostream #include cstring // 策略1针对算术类型的序列化内存拷贝 struct ArithmeticSerializer { template typename T static typename std::enable_ifstd::is_arithmeticT::value::type serialize(const T data, char* buffer) { std::memcpy(buffer, data, sizeof(T)); std::cout Arithmetic serialization used for type: typeid(T).name() std::endl; } }; // 策略2针对拥有serialize成员函数的类型 struct MemberSerializer { template typename T static auto serialize(const T data, char* buffer) - decltype(data.serialize(buffer), void()) { data.serialize(buffer); std::cout Member serialization used for type: typeid(T).name() std::endl; } }; // 默认的序列化器尝试成员函数失败则尝试算术类型。 // 这里简化实际可能需要更复杂的SFINAE或if constexpr。 // 我们使用一个默认模板参数来选择“默认尝试策略”。 template typename T, typename Serializer MemberSerializer // 默认优先尝试成员函数序列化 void serialize(const T data, char* buffer) { // 在实际项目中这里可能需要更精细的SFINAE或C17的if constexpr // 来在ArithmeticSerializer和MemberSerializer之间选择或者让Serializer自己处理。 // 这里为了演示我们假设Serializer已经通过SFINAE做好了选择。 Serializer::serialize(data, buffer); } // 一个自定义类型拥有serialize成员函数 struct MyData { int x; double y; void serialize(char* buf) const { std::memcpy(buf, x, sizeof(x)); std::memcpy(buf sizeof(x), y, sizeof(y)); } }; int main() { char buffer[100]; int i 42; serialize(i, buffer); // 使用默认SerializerMemberSerializer但int没有serialize成员这会导致编译错误吗 // 实际上上面的简单实现会编译错误因为MemberSerializer对int的检查会失败。 // 这正说明了默认模板参数作为“首选策略”的角色。一个健壮的实现需要更复杂的默认序列化器能自动降级。 }这个例子旨在说明设计思路。在实际中你会定义一个更智能的DefaultSerializer它内部使用SFINAE或if constexpr来尝试多种序列化方式并将自身作为默认模板参数。这样用户调用serialize(data, buffer)时默认就能获得一个“尽可能工作”的序列化行为。只有当用户有特殊需求时才需要显式指定第二个模板参数传入自定义的序列化策略。默认模板参数在这里扮演了“默认行为配置开关”的角色极大地简化了通用库的接口。4. 实战陷阱、疑难杂症与最佳实践功能强大也意味着使用时有更多细节需要注意。下面是一些我踩过坑后总结出来的经验。4.1 默认参数与函数重载的决议当存在多个重载的函数模板且它们都有默认模板参数时重载决议可能会变得微妙。编译器会先进行模板参数推导和替换包括使用默认值生成具体的函数签名然后再在这些具体的函数中进行重载决议。template typename T int void foo(T) { std::cout #1\n; } template typename T double void foo(T) { std::cout #2\n; } // 错误重定义 int main() { foo(42); // 调用哪个实际上这两个声明在编译期被视为相同的签名导致重定义错误。 }上面代码编译会报错因为两个foo模板在实例化Tint后都生成了void foo(int)这个具体函数违反了单一定义规则ODR。因此避免为多个可能产生相同实例的重载函数模板提供默认模板参数。更常见的情况是一个带默认模板参数的函数模板和一个普通函数重载template typename T int void bar(T) { std::cout template bar\n; } void bar(int) { std::cout ordinary bar\n; } int main() { bar(42); // 输出哪个 bar(42); // 输出哪个 }bar(42)编译器会优先选择非模板函数void bar(int)因为它是一个完全匹配而模板需要实例化。所以输出ordinary bar。bar(42)明确告诉编译器我们要调用模板版本。编译器使用默认模板参数Tint实例化出void bar(int)然后调用它。所以输出template bar。心得当函数模板与普通函数重载时默认模板参数不会改变重载决议的优先级规则非模板函数优先于模板函数。使用可以强制调用模板版本。4.2 “不可推导上下文”与默认参数的救赎“不可推导上下文”指的是模板参数出现在函数参数列表中一个编译器无法进行类型推导的位置。最常见的例子是template typename T struct Identity { using type T; }; template typename T void func(typename IdentityT::type value) { // T出现在::左边是不可推导上下文 // ... }调用func(42)会失败因为编译器无法从int类型的42反向推导出IdentityT::type中的T是什么。在C11之前这种函数必须显式指定模板参数funcint(42)。有了默认模板参数我们可以为这种“藏在深处”的类型提供一个默认值template typename T int // 为不可推导的T提供默认值 void func(typename IdentityT::type value) { std::cout T is: typeid(T).name() , value: value std::endl; } int main() { // func(42); // 仍然错误因为T不可推导但函数调用会尝试推导失败。 func(42); // 正确使用空的模板参数列表编译器放弃推导直接使用默认值Tint。 func(42); // 如果只有一个默认值且函数参数类型能匹配默认值对应的类型某些编译器能通过不标准规定不行。 // 最安全的做法是使用func()或显式指定funcint(42)。 }这里的关键是func(42)。空的表示“不显式指定任何模板参数但启用模板参数推导”。对于不可推导的T推导失败但由于T有默认值int于是使用默认值实例化出void func(int)成功调用。重要提示对于参数出现在不可推导上下文的函数模板即使提供了默认模板参数在调用时也强烈建议使用语法或显式指定类型以确保代码意图清晰避免不同编译器间的潜在歧义。4.3 默认模板参数的“传染性”与设计权衡默认模板参数具有“传染性”。当你为一个库中的基础函数模板添加了默认参数所有直接或间接调用它的代码都可能受到影响尤其是当它们依赖于模板参数推导时。假设你有一个底层工具函数// utils.h v1.0 template typename T, typename Alloc std::allocatorT void internalHelper(T* data, std::size_t size, const Alloc alloc Alloc());你的库函数使用了它// mylib.h template typename Container void process(Container c) { // ... 一些操作 using value_type typename Container::value_type; // 老代码internalHelper(c.data(), c.size()); // 假设Container有data()和size() // 新代码希望使用默认分配器 internalHelpervalue_type(c.data(), c.size()); // 显式指定value_type让Alloc使用默认值std::allocatorvalue_type }用户代码#include mylib.h #include vector int main() { std::vectorint vec; process(vec); // 正常内部调用internalHelperint, std::allocatorint }现在你将internalHelper升级给Alloc增加了新的默认值MyCustomAlloc假设出于性能优化// utils.h v2.0 template typename T, typename Alloc MyCustomAllocT // 默认值变了 void internalHelper(T* data, std::size_t size, const Alloc alloc Alloc());糟糕的事情发生了。process函数内部调用internalHelpervalue_type(...)时由于只显式指定了第一个模板参数T第二个参数Alloc会使用新的默认值MyCustomAllocvalue_type。这可能导致编译错误如果MyCustomAlloc与Container的分配器不兼容或运行时行为改变分配器不匹配。最佳实践谨慎修改默认值特别是公开库的API修改默认模板参数是破坏二进制兼容性ABI和源码兼容性的高风险操作。显式指定以隔离变化在内部调用链中如果下层函数的默认模板参数可能变化上层函数最好显式指定所有模板参数而不是依赖默认值。例如process函数应该写成template typename Container void process(Container c) { using value_type typename Container::value_type; using allocator_type typename Container::allocator_type; // 获取容器的分配器类型 internalHelpervalue_type, allocator_type(c.data(), c.size()); // 显式传递不依赖默认值 }文档化默认行为在头文件注释中清晰说明每个默认模板参数的语义和选择它的理由。4.4 与变参模板结合使用C11的变参模板Variadic Templates与默认模板参数是天作之合。你可以为变参包前面的某些固定参数提供默认值。// 一个日志函数可以指定多个标签变参并有一个默认的输出流 template typename... Tags, typename OutputStream std::ostream void log(OutputStream os std::cout, const std::string message) { os [; // 折叠表达式 (C17) 打印所有标签 (os ... typeid(Tags).name()) ] ; // C17 fold expression os message std::endl; } // 调用 int main() { log(Hello); // 错误无法推导Tags...因为Tags...在OutputStream之前。 }这里有个陷阱模板参数包Tags...必须在所有非包参数之前或者被放在可以推导的位置。上面的代码中Tags...是不可推导的因为没有对应的函数参数并且它位于有默认值的OutputStream之前。调用log(Hello)时编译器试图推导Tags...和OutputStream但Hello是const char*无法匹配OutputStream默认是std::ostream和后面的const std::string两个参数因此失败。正确的设计通常是将可推导的、或有关键作用的参数放在前面将默认参数和参数包放在后面或者通过将参数包与特定参数绑定如使用std::tuple来使其可推导。一个更实用的日志例子// 将Tags...放在最后并为OutputStream提供默认值 template typename OutputStream std::ostream, typename... Tags void log(const std::string message, OutputStream os std::cout) { os [; // 这里简化处理Tags实际可能需要遍历打印 os ...Tags...; os ] message std::endl; } // 调用 log(Hello); // 使用默认的std::cout核心要点当函数模板同时拥有默认模板参数和变参模板参数时参数包的推导优先级和位置需要精心设计否则很容易导致调用时推导失败。一个通用的建议是将最可能需要用户显式指定或最核心的模板参数放在前面将带有默认值的、或辅助性的参数包括参数包放在后面。5. 性能考量、调试技巧与代码维护5.1 对编译时间和代码膨胀的影响默认模板参数本身不会增加运行时开销因为它是在编译期决定的。但是它可能间接影响编译时间和生成的代码体积二进制膨胀。编译时间更多的默认参数意味着编译器在解析模板声明和进行推导时需要考虑更多的可能性。如果一个头文件中大量使用带有复杂默认模板参数的函数模板特别是这些默认值本身又是复杂的模板实例如std::vectorstd::pairT1, T2可能会增加头文件的解析负担。然而在现代编译器中这种影响通常很小除非在极端庞大的模板元编程库中。代码膨胀默认模板参数可能导致生成更多版本的模板实例。例如template typename T, typename Alloc std::allocatorT void f(T)。如果用户在不同翻译单元中分别以fint()和fint, std::allocatorint()的形式调用理论上会实例化两个完全相同的函数因为默认参数让它们等价。优秀的链接器如GCC/Clang的链接器会进行重复代码消除COMDAT folding将相同的实例合并因此实际膨胀可控。但为了保险起见在性能敏感的代码中尽量保持调用方式一致。调试建议使用GCC或Clang编译时可以添加-ftime-report或-ftime-traceClang来查看模板实例化耗时。如果发现某个带有默认模板参数的函数模板实例化异常耗时可以考虑是否默认值过于复杂或者是否被不必要地频繁实例化。5.2 在IDE中的可读性与探索性现代IDE如CLion、Visual Studio、Qt Creator对C模板的支持越来越好包括对默认模板参数的显示。悬停提示将鼠标悬停在函数名上时IDE通常会显示完整的函数签名包括默认模板参数。这有助于快速了解函数的默认行为。代码补全当你输入函数名和时IDE的补全列表可能会提示可用的模板参数及其默认值。跳转到定义对于带有默认模板参数的函数跳转到定义能直接看到默认值是什么比查看文档更直接。为了最大化利用这些IDE特性建议保持默认值的简洁和直观避免使用过于复杂或嵌套很深的类型作为默认值。例如std::vectorT比MyComplexPolicyT, AnotherDefaultT更友好。使用类型别名如果默认值很复杂可以定义一个清晰的类型别名。template typename T using DefaultAllocatorFor MyCustomAllocT, PoolSize1024, ThreadSafePolicy; template typename T, typename Alloc DefaultAllocatorForT void process(T* data, Alloc alloc Alloc());这样在IDE提示中用户会看到Alloc DefaultAllocatorForT而不是一长串复杂的模板参数点击别名还能跳转到定义了解其具体组成。5.3 版本兼容性与ABI考虑如前所述修改默认模板参数是破坏性变更。在设计和维护库时初始设计时深思熟虑尽可能预测未来需求为可能扩展的策略预留模板参数并设置一个稳定、通用的默认值。通过版本命名空间隔离变更如果必须修改默认行为考虑提供新版本的函数放在不同的命名空间或使用不同的函数名而不是直接修改原函数。// v1 namespace (stable) namespace v1 { template typename T, typename Alloc StdAllocatorT void func(); } // v2 namespace with new default namespace v2 { template typename T, typename Alloc CustomAllocatorT void func(); } // 或者通过后缀区分 template typename T, typename Alloc StdAllocatorT void func_v1(); template typename T, typename Alloc CustomAllocatorT void func_v2();文档中明确标注在API文档中用醒目的方式说明哪些默认模板参数在未来的版本中可能发生变化哪些是稳定的。5.4 测试策略测试带有默认模板参数的函数模板需要覆盖多种调用方式完全使用默认参数func(args)。部分指定部分使用默认funcT1, T2(args)其中T3,T4等使用默认值。显式指定所有参数包括与默认值相同的funcT1, T2, DefaultT3, DefaultT4(args)。使用语法func(args)。测试与函数重载的交互。**测试默认参数导致的“不可推导上下文”**是否正确工作。使用类型特征type traits和静态断言static_assert可以在编译期验证默认参数是否按预期工作。例如可以写一个测试特质template typename Func, typename ExpectedDefault struct check_default_arg; // 通过SFINAE或if constexpr在测试代码中验证Func的某个模板参数默认值是否是ExpectedDefault。虽然完全自动化测试模板默认值比较困难但通过组合不同的测试用例可以有效地保证其正确性。6. 从C11到C14/17/20的演进C11引入的函数模板默认参数是一个良好的开端后续标准在此基础上做了增强和补充。C14: 没有对函数模板默认参数本身做重大修改但引入了泛型Lambda和变量模板它们与默认模板参数结合可以创造更灵活的代码结构。例如Lambda表达式不能直接有模板参数C20之前但可以通过包装在带默认模板参数的泛型函数对象中来模拟。C17: 引入了“类模板参数推导”CTAD和if constexpr。CTAD主要针对类模板但间接影响了函数模板设计。if constexpr则让在函数模板内部基于类型条件选择不同代码路径变得更容易有时可以替代需要通过不同默认模板参数来实现的策略选择使代码更内聚、更易读。C20: 带来了“概念”Concepts这是对模板参数约束的革命性改进。你可以为默认模板参数附加概念约束确保默认类型满足某些要求从而在编译期获得更清晰的错误信息。template std::integral T int // C20: 默认模板参数T必须是整数类型默认int符合。 void process_integer(T value) { ... }这大大增强了默认模板参数的安全性和表达力。同时C20允许Lambda表达式拥有模板参数这使得一些需要默认模板参数的场景可以用更简洁的Lambda来替代。展望函数模板的默认模板参数从C11的“支持”到后续标准的“增强”体现了C向更安全、更清晰、更易用的泛型编程发展的趋势。掌握好C11的基础就能平滑地过渡到后续更高级的用法。
返回列表