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

资讯详情

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

C++模板编程:从泛型函数到编译期计算的实战指南

C++模板编程:从泛型函数到编译期计算的实战指南 1. 从“重复造轮子”到“一劳永逸”为什么我们需要模板如果你写过一段时间的C尤其是写过一些需要处理不同类型数据的通用函数或数据结构你大概率经历过这种痛苦为了给整数、浮点数、字符串分别实现一个功能完全相同的“找最大值”函数你不得不写三个几乎一模一样的函数体只是参数类型和返回值类型不同。代码看起来就像这样int maxInt(int a, int b) { return (a b) ? a : b; } double maxDouble(double a, double b) { return (a b) ? a : b; } std::string maxString(const std::string a, const std::string b) { return (a b) ? a : b; }这还只是三种类型如果未来要支持自定义的MyClass只要它重载了运算符理论上这个max函数也应该能工作难道要再复制粘贴一遍把类型改成MyClass吗这种代码的维护是一场噩梦当你发现max函数的比较逻辑有个边界条件需要修复时你得把上面所有函数都改一遍稍有遗漏就会引入bug。这种场景下一个朴素的想法就会冒出来能不能把“类型”也作为一种参数让编译器在编译时根据我们实际使用的类型自动“生成”出对应版本的代码呢这就是C模板Template诞生的最直接动机。它解决的正是“代码复用”中因类型不同而导致的冗余问题。模板允许你编写与类型无关的通用代码它是一种编译期多态的工具。与运行时多态虚函数不同模板的多态性在编译阶段就已经通过代码生成实例化确定了因此没有任何运行时开销这是它性能强大的关键。简单来说使用模板后上面的max函数只需要写一次template typename T T max(T a, T b) { return (a b) ? a : b; }当你调用max(10, 20)时编译器看到实参是int就会将模板参数T替换为int生成一个int max(int, int)的函数版本。调用max(3.14, 2.71)时则生成double版本。这个过程叫做模板实例化。所以模板的核心价值在于“泛型编程”。它让你从“为特定类型编写算法”的思维跃升到“为抽象概念编写算法”的思维。这个抽象概念就是“所有满足某些操作比如可比较、可拷贝等的类型”。C标准库STL就是泛型编程最伟大的实践其中的容器vector,map、算法sort,find都是模板它们能适用于任何符合要求的类型。2. 函数模板让算法与类型脱钩函数模板是模板中最基础、最常用的形式。它的目标是将一个函数逻辑抽象出来使其能处理多种类型。2.1 基本语法与实例化过程一个函数模板的定义以关键字template开始后跟一个尖括号包围的模板参数列表。列表里可以有一个或多个参数最常见的是typename T或等价的class T。template typename T // 模板参数声明T是一个类型参数 T add(T a, T b) { // 函数签名和主体中使用T return a b; }这里typename T声明了一个类型占位符T。在函数模板内部T可以像任何具体的类型如int,double一样使用。实例化是隐式发生的。当你调用add(5, 3)时编译器进行以下推导实参5和3都是int类型。编译器尝试将模板参数T与实参类型匹配推导出T int。编译器在内部生成一个int add(int, int)的函数实体并编译它。程序运行时调用的就是这个新生成的、专用于int的函数。这个过程对程序员是透明的你感觉就像在调用一个普通函数。但本质上编译器为你“写”了新的代码。2.2 类型推导与显式指定大多数时候编译器都能根据传入的实参完美推导出模板参数类型。但有时也会遇到需要手动指定的情况。场景一类型推导失败或歧义template typename T T max(T a, T b) { return (a b) ? a : b; } int main() { int a 1; double b 2.5; // auto m max(a, b); // 错误编译器无法推导T应该是int还是double auto m maxdouble(a, b); // 正确显式指定T为doublea会被隐式转换为double return 0; }当两个实参类型不同时编译器无法确定统一的T推导失败。此时必须在函数名后使用尖括号显式指定模板参数。场景二调用没有参数依赖的模板函数template typename T void printType() { std::cout typeid(T).name() std::endl; } int main() { // printType(); // 错误无法从空参数列表推导T printTypeint(); // 输出i (表示int) printTypestd::string(); // 输出NSt7__cxx1112basic_string... return 0; }printType函数根本不接受参数其行为完全依赖于类型T。这种情况下必须显式指定类型。场景三希望使用与推导结果不同的类型有时实参是int但你希望函数内部以long来运算以避免溢出也可以显式指定。template typename T T square(T x) { return x * x; } int main() { int big 1000000; // auto r square(big); // r是int可能溢出 auto r squarelong long(big); // 显式指定用long long计算安全 return 0; }2.3 非类型模板参数将值作为模板参数模板参数不仅仅是类型也可以是整型、枚举、指针或引用等值。这允许你在编译期就确定一些常量。// 定义一个固定大小的数组包装类大小N在编译时确定 template typename T, std::size_t N class FixedArray { public: T operator[](std::size_t index) { return data_[index]; } const T operator[](std::size_t index) const { return data_[index]; } std::size_t size() const { return N; } // 编译期常量 private: T data_[N]; // 数组大小在编译期已知 }; int main() { FixedArrayint, 10 arr10; // 一个大小为10的int数组 FixedArraydouble, 100 arr100; // 一个大小为100的double数组 // FixedArrayint, n arrDynamic; // 错误n必须是编译期常量 return 0; }这里N是一个非类型模板参数。它的优势在于性能像size()这样的函数可以直接内联返回常量编译器能进行更好的优化。类型安全FixedArrayint, 10和FixedArrayint, 20是不同的类型不能互相赋值避免了大小不匹配的错误。可用于模板元编程在编译期进行计算。注意非类型模板参数必须是编译期常量。C17后auto也可以作为非类型模板参数的类型让编译器自动推导类型如template auto Value class Foo {};。2.4 默认模板参数与模板特化和函数参数一样模板参数也可以有默认值。// 默认分配器为std::allocatorT template typename T, typename Allocator std::allocatorT class SimpleVector { // ... 使用Allocator分配内存 }; int main() { SimpleVectorint v1; // 使用默认的std::allocatorint SimpleVectorint, MyCustomAllocatorint v2; // 使用自定义分配器 return 0; }模板特化则是为特定的模板参数提供定制化的实现。当通用模板无法满足某些类型的特殊需求时就需要特化。// 通用模板 template typename T struct IsPointer { static const bool value false; }; // 对指针类型的特化偏特化 template typename T struct IsPointerT* { static const bool value true; }; // 全特化示例为const char*提供特殊的比较函数 template bool compare(const char* a, const char* b) { return strcmp(a, b) 0; }特化是模板元编程和类型 Traits 的基础它让模板具备了更强的表现力和灵活性。3. 类模板构建通用容器与数据结构如果说函数模板让算法泛型化那么类模板就让数据结构泛型化。C标准库中的vector,list,map等都是类模板的典范。3.1 定义与使用类模板类模板的声明和定义通常都放在头文件中因为编译器需要在每个使用它的翻译单元中看到完整的定义以便实例化。// my_vector.h #ifndef MY_VECTOR_H #define MY_VECTOR_H template typename T class MyVector { public: // 构造函数 MyVector(std::size_t capacity 10); // 析构函数 ~MyVector(); // 拷贝构造函数需要深拷贝 MyVector(const MyVector other); // 赋值运算符 MyVector operator(const MyVector other); // 元素访问 T operator[](std::size_t index); const T operator[](std::size_t index) const; // 容量操作 void push_back(const T value); std::size_t size() const; std::size_t capacity() const; private: T* data_; std::size_t size_; std::size_t capacity_; void resize(std::size_t new_capacity); }; // 类模板的成员函数定义也必须放在头文件中并且同样是模板 template typename T MyVectorT::MyVector(std::size_t capacity) : data_(new T[capacity]), size_(0), capacity_(capacity) {} template typename T MyVectorT::~MyVector() { delete[] data_; } template typename T void MyVectorT::push_back(const T value) { if (size_ capacity_) { resize(capacity_ * 2); } data_[size_] value; // 这里要求T类型支持赋值操作 } // 其他成员函数定义... #endif // MY_VECTOR_H使用这个MyVector时你必须指定具体的类型#include my_vector.h int main() { MyVectorint intVec; // 实例化一个存储int的MyVector intVec.push_back(1); intVec.push_back(2); MyVectorstd::string strVec; // 实例化一个存储string的MyVector strVec.push_back(Hello); strVec.push_back(Template); // MyVector vec; // 错误类模板必须指定模板参数 return 0; }3.2 类模板中的成员函数与友元类模板的成员函数包括构造函数、析构函数和其他所有函数在定义时都必须带上模板头template typename T并且使用MyVectorT::作为作用域限定。友元声明在类模板中会稍微复杂一些。如果你希望一个普通函数或另一个模板成为类模板的友元需要仔细处理。template typename T class MyVector { // 声明一个普通函数为友元该函数不是模板 friend void printSize(const MyVectorT vec); // 声明一个函数模板为友元每个实例都是对应MyVector实例的友元 template typename U friend bool operator(const MyVectorU lhs, const MyVectorU rhs); }; // 普通友元函数的定义 template typename T void printSize(const MyVectorT vec) { std::cout Size: vec.size_ std::endl; // 可以访问私有成员size_ } // 友元函数模板的定义 template typename U bool operator(const MyVectorU lhs, const MyVectorU rhs) { if (lhs.size_ ! rhs.size_) return false; // 访问私有成员 for (std::size_t i 0; i lhs.size_; i) { if (lhs.data_[i] ! rhs.data_[i]) return false; } return true; }3.3 依赖类型与typename关键字在类模板内部有时编译器无法判断一个标识符是类型还是值。这时必须使用typename关键字来显式告知编译器“这是一个类型”。template typename T class Container { public: // 假设T::iterator是一个类型比如在STL容器中 // 不加typename编译器会认为iterator是T的静态成员变量 typename T::iterator begin() { // 正确使用typename声明依赖类型 return data_.begin(); } // 另一个常见场景在模板中定义类型别名 using ValueType typename T::value_type; // T::value_type也是一个依赖类型 private: T data_; };这个规则被称为“typename消歧义”。简单记法在模板定义中任何依赖于模板参数T的嵌套类型如T::XXX前面都需要加上typename除非它出现在基类列表或成员初始化列表中。3.4 类模板的继承与组合类模板可以继承自另一个类模板也可以作为其他类的成员。这为构建复杂的泛型结构提供了可能。// 基类模板 template typename T class Base { protected: T value_; public: Base(const T val) : value_(val) {} virtual void print() const { std::cout value_ std::endl; } }; // 派生类模板 template typename T, typename U class Derived : public BaseT { // 继承自BaseT public: Derived(const T t, const U u) : BaseT(t), extra_(u) {} void print() const override { BaseT::print(); // 调用基类方法 std::cout Extra: extra_ std::endl; } private: U extra_; }; // 使用组合 template typename T class Wrapper { public: Wrapper(const T obj) : object_(obj) {} void doSomething() { object_.print(); // 要求T类型有print()成员函数 } private: T object_; // 组合一个T类型的对象 };在派生类中如果使用基类中定义的名称如value_有时也需要使用this-或BaseT::来显式指明因为编译器在解析模板时可能无法确定这些名称的依赖关系。4. 可变参数模板处理任意数量与类型的参数C11引入的可变参数模板是模板编程的一次飞跃。它允许模板接受任意数量、任意类型的参数是实现像std::tuple,std::function,std::make_shared等强大工具的基础。4.1 语法基础参数包与包展开可变参数模板使用省略号...的语法。typename... Args声明一个模板参数包Args它可以包含零个或多个类型参数。Args... args声明一个函数参数包args它是模板参数包Args的实例化。在模式后面跟...表示将这个模式应用于参数包中的每一个元素这叫做包展开。// Args是一个模板参数包可以接受0个或多个类型 template typename... Args void print(Args... args) { // args是一个函数参数包 // 无法直接操作args需要借助包展开或递归 }4.2 递归展开编译期的“循环”处理参数包最经典的方式是递归。我们需要一个终止函数来处理空参数包的情况。// 终止函数当参数包为空时调用 void print() { std::cout std::endl; // 打印换行结束递归 } // 可变参数模板函数处理第一个参数然后递归处理剩余参数 template typename T, typename... Args void print(T first, Args... rest) { std::cout first ; // 打印第一个参数 print(rest...); // 递归调用展开剩余参数包 } int main() { print(1, 3.14, Hello, A); // 输出1 3.14 Hello A print(); // 调用终止函数 return 0; }编译器会实例化出一系列函数print(int, double, const char*, char)print(double, const char*, char)print(const char*, char)print(char)print()递归在编译期就完成了运行时只是一连串的函数调用。4.3 折叠表达式C17的简化利器C17引入了折叠表达式它允许使用二元运算符直接对参数包进行化简大大简化了可变参数模板的代码。// 使用折叠表达式计算所有参数的和 template typename... Args auto sum(Args... args) { return (args ...); // 一元右折叠(arg1 (arg2 (arg3 ...))) // 等价于 return (args ... 0); // 带初始值的右折叠 } // 使用折叠表达式打印所有参数比递归简洁得多 template typename... Args void printFold(Args... args) { (std::cout ... args) std::endl; // 二元左折叠(((cout arg1) arg2) ...) } int main() { std::cout sum(1, 2, 3, 4, 5) std::endl; // 输出15 printFold(1, , 2, , 3); // 输出1 2 3 return 0; }折叠表达式支持四种形式( pack op ... )一元右折叠( ... op pack )一元左折叠( pack op ... op init )二元右折叠( init op ... op pack )二元左折叠 其中op是32种允许的运算符之一如,-,*,/,,,等。4.4 完美转发与std::forward可变参数模板的一个关键应用场景是实现“完美转发”——将参数以原始的值类别左值、右值传递给另一个函数。这需要结合万能引用和std::forward。// 一个简单的工厂函数模板 template typename T, typename... Args T create(Args... args) { // Args... 是万能引用参数包 // 使用std::forward保持参数的值类别左值/右值 return T(std::forwardArgs(args)...); } class Widget { public: Widget(int a, double b, const std::string c) { std::cout 构造Widget: a , b , c std::endl; } Widget(const Widget) delete; // 禁止拷贝 Widget(Widget) { std::cout 移动构造Widget std::endl; } }; int main() { auto w1 createWidget(42, 3.14, test); // 传递右值调用构造函数 Widget temp(1, 2.0, temp); // auto w2 createWidget(temp); // 错误因为Widget禁止拷贝 auto w3 createWidget(std::move(temp)); // 正确传递右值引用调用移动构造函数 return 0; }std::forwardArgs(args)...这个表达式会展开为std::forwardArg1(arg1), std::forwardArg2(arg2), ...确保每个参数都能以正确的值类别传递给T的构造函数。这是实现像std::make_unique,std::make_shared这类工厂函数的核心技术。5. 模板元编程初窥让计算发生在编译期模板元编程是利用模板在编译期执行计算的技术。它基于一个核心事实模板实例化是编译期的过程。通过特化、递归和类型计算我们可以在编译期完成一些原本需要在运行时完成的工作比如计算斐波那契数列、判断类型特性等。5.1 编译期整数计算以斐波那契数列为例// 主模板定义通用的斐波那契计算 template unsigned N struct Fibonacci { static const unsigned long long value FibonacciN-1::value FibonacciN-2::value; }; // 全特化终止条件 Fib(0) template struct Fibonacci0 { static const unsigned long long value 0; }; // 全特化终止条件 Fib(1) template struct Fibonacci1 { static const unsigned long long value 1; }; int main() { // 这些计算在编译期就已经完成 std::cout Fibonacci0::value std::endl; // 0 std::cout Fibonacci1::value std::endl; // 1 std::cout Fibonacci10::value std::endl; // 55 std::cout Fibonacci50::value std::endl; // 12586269025 // 注意过大的N可能导致编译期递归深度过大编译失败或极慢。 return 0; }这里FibonacciN::value是一个编译期常量。编译器会像展开递归函数一样实例化出Fibonacci10,Fibonacci9...直到Fibonacci0和Fibonacci1并计算出最终结果。程序运行时直接使用这个已经计算好的常量没有任何函数调用开销。5.2 类型计算与类型萃取模板元编程更强大的用途是进行类型计算和推导。标准库中的type_traits头文件提供了大量此类工具。// 一个简单的类型萃取移除指针 template typename T struct RemovePointer { using type T; // 默认情况类型就是T本身 }; template typename T struct RemovePointerT* { using type T; // 对于指针类型T*移除一层指针 }; template typename T struct RemovePointerT* const { using type T; // 处理常量指针 }; template typename T struct RemovePointerT* volatile { using type T; // 处理易变指针 }; template typename T struct RemovePointerT* const volatile { using type T; // 处理常量易变指针 }; // 使用别名模板简化调用C11 template typename T using RemovePointer_t typename RemovePointerT::type; int main() { RemovePointer_tint* a; // a的类型是int RemovePointer_tconst double* b; // b的类型是const double RemovePointer_tint c; // c的类型是int return 0; }这种技术在编写通用库时极其有用。例如一个容器可能需要知道其元素类型的“值类型”去掉引用、指针等修饰这时就可以用类型萃取来获取。5.3 SFINAE 与std::enable_ifSFINAESubstitution Failure Is Not An Error替换失败并非错误是模板重载决议中的一个核心规则。它允许编译器在尝试匹配模板时忽略那些会导致编译错误的替换而不是直接报错。利用这一点我们可以根据类型的特性来有选择地启用或禁用某个模板。std::enable_if是应用SFINAE的经典工具。// 函数1适用于有size()成员函数的类型如容器 template typename T auto getSize(const T container) - decltype(container.size(), std::size_t()) { std::cout 调用容器版本 std::endl; return container.size(); } // 函数2适用于原生数组 template typename T, std::size_t N std::size_t getSize(T (array)[N]) { std::cout 调用数组版本 std::endl; return N; } // 使用enable_if的进阶示例根据类型是否有特定成员函数选择实现 template typename T, typename void // 默认情况没有serialize成员 struct Serializer { static void serialize(const T obj) { std::cout 通用序列化可能不支持 std::endl; } }; template typename T struct SerializerT, std::void_tdecltype(std::declvalT().serialize()) { static void serialize(const T obj) { std::cout 调用成员serialize() std::endl; obj.serialize(); } }; class WithSerialize { public: void serialize() const { std::cout WithSerialize::serialize std::endl; } }; class WithoutSerialize {}; int main() { std::vectorint vec{1,2,3}; int arr[5] {0}; std::cout getSize(vec) std::endl; // 调用容器版本输出3 std::cout getSize(arr) std::endl; // 调用数组版本输出5 WithSerialize ws; WithoutSerialize wos; SerializerWithSerialize::serialize(ws); // 调用特化版本 SerializerWithoutSerialize::serialize(wos); // 调用通用版本 return 0; }在C17之后if constexpr和C20的concept提供了更清晰、更易用的方式来实现条件编译但理解SFINAE对于阅读老代码和深入理解模板机制仍然非常重要。6. 模板实战从概念到代码的避坑指南理论懂了上手写模板代码时依然会踩很多坑。这部分分享一些我实践中总结的经验和常见问题的解法。6.1 链接错误为什么模板实现要放在头文件这是模板新手最常遇到的问题。你写了一个完美的类模板将声明放在.h定义放在.cpp然后在另一个.cpp文件中使用它链接时却报错“未定义的引用”。原因模板不是普通的函数或类。编译器只有在看到模板被使用即实例化时才会根据具体的模板参数生成真正的代码。如果你在a.cpp中定义了模板函数在b.cpp中使用它编译器在编译b.cpp时只看到了模板的声明看不到定义因此无法实例化。链接器在链接时找不到b.cpp所期望的那个实例化版本比如MyClassint的实现于是报错。解决方案推荐将模板的定义全部放在头文件中。这是最常见和简单的方法。确保使用该模板的所有源文件都能#include这个头文件编译器在每个翻译单元都能看到完整定义并进行实例化。使用显式实例化。在模板定义的.cpp文件末尾显式地告诉编译器你需要哪些实例化版本。// my_template.cpp template typename T T add(T a, T b) { return a b; } // 显式实例化 template int addint(int, int); template double adddouble(double, double);这样编译器会在my_template.cpp中生成int和double版本的add函数。其他文件使用这两个版本时就能正确链接。缺点是你要预先知道所有需要用到的类型。C11的extern template。在头文件中声明模板在某个源文件中定义并实例化在其他使用该实例的源文件中用extern template声明来阻止编译器重复实例化以加快编译速度。// my_template.h template typename T T add(T a, T b); // 告诉编译器addint的定义在其他地方你别实例化 extern template int addint(int, int); // user.cpp #include my_template.h int main() { add(1, 2); } // 链接时去找其他地方定义的addint // my_template.cpp #include my_template.h template typename T T add(T a, T b) { return a b; } template int addint(int, int); // 在这里实例化6.2 编译错误依赖名称与template关键字当模板代码嵌套较深时可能会遇到“依赖名称”的问题。编译器在解析模板时需要知道一个名称是类型、模板还是值。template typename T class Outer { public: template typename U class Inner { public: static void foo() {} }; }; template typename T void bar() { OuterT::InnerT obj; // 错误编译器不知道Inner是类型还是静态成员 OuterT::InnerT::foo(); // 错误编译器不知道foo是函数模板还是静态成员函数 }对于依赖模板参数T的名称如OuterT::Inner编译器在第一次解析时此时T具体是什么还不知道无法确定Inner的性质。你需要使用typename和template关键字来消除歧义。template typename T void bar() { typename OuterT::template InnerT obj; // 正确Inner是类型 typename OuterT::template InnerT::foo(); // 正确但foo是函数调用需要实例化 // 或者如果foo是静态成员函数 OuterT::template InnerT::foo(); }typename告诉编译器OuterT::Inner是一个类型。template告诉编译器InnerT是一个模板因为Inner本身也是模板。6.3 代码膨胀如何控制模板实例化带来的体积增长模板在带来灵活性和性能的同时也可能导致“代码膨胀”——编译器为每种类型参数都生成一份独立的代码如果实例化的类型很多最终的可执行文件可能会非常大。缓解策略共性抽取将模板类中不依赖类型参数的代码移到非模板基类中。// 非模板基类存放公共代码和数据 class VectorBase { protected: void* data_; std::size_t size_; std::size_t capacity_; void resizeImpl(std::size_t new_capacity); // 内存管理逻辑 }; // 模板派生类只处理类型相关的操作 template typename T class Vector : private VectorBase { public: T operator[](std::size_t index) { return static_castT*(data_)[index]; } void push_back(const T value) { if (size_ capacity_) resizeImpl(capacity_ * 2); new (static_castT*(data_) size_) T(value); // placement new size_; } // ... 类型相关的操作 };这样内存管理的代码只在VectorBase中存在一份Vectorint和Vectordouble共享这部分代码。使用通用指针对于容器可以考虑使用void*来存储数据在内部进行类型转换。但这会牺牲类型安全需要非常小心。显式实例化常用类型如果你知道项目中最常用的就是int,double,std::string等少数几种类型可以使用前面提到的显式实例化并鼓励团队只使用这些类型。对于不常用的类型在链接时会报错从而提醒开发者。编译器优化现代编译器如GCC、Clang具有“相同代码折叠”的链接时优化能力能够识别出不同实例化版本中完全相同的机器代码并合并它们。开启-O2,-Os或LTO链接时优化有助于减少最终体积。6.4 调试难题如何读懂天书般的模板错误信息GCC或Clang的模板错误信息常常长达几十甚至上百行充斥着各种内部类型名让人望而生畏。应对技巧从最后一行看起编译器错误信息通常是“瀑布式”的最后一行往往是最根本的原因。比如“no matching function for call to ‘foo’”说明函数调用不匹配。寻找第一个“error:”在一大段信息中找到第一个标为“error”的行这通常是问题的起点。关注你写的代码行号编译器会指出在你的源文件中哪一行触发了错误。从这一行开始检查。简化类型名错误信息中的类型名可能非常长如std::__cxx11::basic_stringchar, std::char_traitschar, std::allocatorchar 。可以尝试在脑海中将其简化为std::string来理解。使用静态断言static_assert进行提前检查在模板代码中加入static_assert可以在编译早期就给出清晰的错误信息。template typename T void processContainer(const T container) { // 在函数开始处检查类型要求 static_assert(std::is_same_vtypename T::value_type, int, processContainer only works with containers of int); // ... 函数逻辑 }分治法如果错误信息涉及深层的模板展开尝试将调用层层剥离先注释掉部分代码创建一个最小的、能复现错误的例子这样错误信息会更清晰。使用C20 Concepts如果你在使用C20或更高版本一定要用Concepts它能将类型约束写在明面上编译器会在调用时给出非常清晰的错误信息指出哪个约束不满足彻底告别模板元编程的“天书”。template std::integral T // 要求T是整型 T add(T a, T b) { return a b; } add(3.14, 2.71); // 错误信息会很清晰double不满足std::integral约束。模板是C中最强大也最复杂的特性之一。它就像一把双刃剑用好了可以写出极其高效、灵活和优雅的通用代码用不好则会带来编译时间暴涨、错误信息晦涩、代码难以调试和维护等问题。我的建议是先从模仿标准库的简单用法开始理解函数模板和类模板的基本原理然后在实际项目中谨慎地应用。对于复杂的模板元编程和可变参数模板除非你正在编写基础库或框架否则应优先考虑使用更简单、更易读的替代方案。记住代码首先是写给人看的其次才是给机器执行的。
返回列表