记录一下学习C++模板时的知识点
C++中.template
的用法 template
关键字用于成员函数或嵌套模板名称,帮助编译器正确解析模板代码。在调用带有模板参数的模板函数时,编译器无法直接判断 <
是模板参数列表的开始还是小于号。此时必须用 .template
显式指明后续内容是模板。
1 2 3 4 5 6 7 8 9 10 11 template <typename T>struct Foo { template <typename U> void bar () {}}; template <typename T>void func (Foo<T>& f) { f.bar <int >(); f.template bar <int >(); }
变量模板 在C++14之后,变量也可以模板参数化,称为变量模板。
1 2 3 4 5 template <typename T>constexpr T pi{3.1415926535897932385 };std::cout << pi<double > << ’\n’; std::cout << pi<float > << ’\n’;
模板作为模板参数的用法 比如自定义容器的底层存储数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <vector> #include <list> template <typename T, template <typename > class Container >class Stack {private : Container<T> elements; public : void push (const T& val) { elements.push_back (val); } }; int main () { Stack<int , std::vector> s1; Stack<double , std::list> s2; return 0 ; }
std::vector
和 std::list
是模板,符合 template <typename> class Container
的约束。
这里需要注意的是,在C++17之前Container前的修饰符只能是class
,C++17之后可以用typename
。单个模板参数没有用到时可以省略。
其实也可以实例化之后再传参
1 2 3 4 5 6 7 template <typename T, typename Container = std::deque<T>>class Stack { private : Container elems; public : ... }
std::enable_if
的用法std::enable_if
是 C++ 模板元编程中基于 SFINAE 的关键工具,用于 条件编译 。
定义 1 2 3 4 5 template <bool Condition, typename T = void >struct enable_if;
限制模板编译的用法 模板参数条件编译 1 2 3 template <typename T, typename = std::enable_if_t <(sizeof (T) > 4 )>>void foo () {}
如果sizeof(T)≤4
这个模板函数就不会被编译,否则会被展开成
1 2 3 template <typename T, typename = void >void foo () {}
这里的typename = void
没有实际的意义
返回值条件编译 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T>typename std::enable_if<std::is_integral<T>::value, void >::type foo (T x) { std::cout << "Integral: " << x << std::endl; } template <typename T>typename std::enable_if<std::is_floating_point<T>::value, void >::type foo (T x) { std::cout << "Floating: " << x << std::endl; } foo (42 ); foo (3.14 ); foo ("hello" );
这里是在返回值位置上实现的条件编译,编译时对该条件进行判断,如果满足则进行编译,不满足则曲线编译。
模板特化控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename T, typename = void >struct Bar { static void print () { std::cout << "Default (disabled)\n" ; } }; template <typename T>struct Bar <T, typename std::enable_if<has_iterator<T>::value>::type> { static void print () { std::cout << "Iterable type\n" ; } }; Bar<int >::print (); Bar<std::vector<int >>::print ();
参数按值传递还是按引用传递 函数模板参数优先使用按值传递,按值传递可以避免很多麻烦,但有些情况需要考虑按引用传递。
对象不允许拷贝
需要使用参数引用返回数据
参数的左值右值属性需要转发(完美转发)
需要减少拷贝优化性能
按值传递 即使是按值传递也可以使用移动语义减少拷贝
1 2 3 4 5 6 7 8 9 10 template <typename T>void printV (T arg) { ... } printV (std::string ("hi" ));printV (returnString ());std::string s = "hi" ; printV (std::move (s));
按值传递有一个重要的特性是参数类型会退化,裸数组会退化成指针,const和volatile等限制符会被移除。
按引用传递 按引用传递参数时,其类型不会退化(decay)。也就是说不会把裸数组转换为指针,也不会移除 const 和 volatile 等限制符。
但引用传递有多种情况需要考虑,使用上更加复杂。例如const常量引用和非常量引用,左值引用和右值引用。
const常量引用 const常量引用 的问题较少,推荐一般情况下使用常量引用
1 2 3 4 5 6 std::string returnString () ;std::string s = "hi" ; printR (s); printR (std::string ("hi" )); printR (returnString ()); printR (std::move (s));
由于调用参数被声明为 T const &,被推断出来的模板参数 T 的类型将不包含 const
非const常量引用 非const常量引用通常使用于需要用参数返回值的场景,由于非const修饰,因此不能传递临时变量和move变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <string> template <typename T> void outR (T& arg) {} std::string returnString () { return std::string ("hello" ); } const std::string returnConstString () { return std::string ("hello" ); } int main () { int x = 42 ; const int cx = 42 ; outR (x); outR (cx); outR ("hello" ); outR (returnConstString ()); outR (std::move (cx)); }
模板在推导时可能会推导出const属性,所以模板在修改参数值时可能会遇到错误,为了避免这个错误可以使用编译器检查static_assert
或者条件编译std::enable_if
。
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename T>void outR (T& arg) { static_assert (!std::is_const<T>::value, "out parameter of foo<T>(T&)is const" ); … } template <typename T,typename = std::enable_if_t <!std::is_const<T>::value>void outR (T& arg) { … }
完美转发引用传递 还有一种更复杂的引用叫完美转发,体现为保留参数在传递时的性质,如果实参是左值传递进来的形参也是左值,如果是右值形参也是右值,同时也能保留const的特性。
1 2 3 4 5 6 7 8 9 10 11 template <typename T>void passR (T&& arg) { … } std::string s = "hi" ; passR (s); passR (std::string ("hi" )); passR (returnString ()); passR (std::move (s)); passR (arr);
完美转发也有缺点,如果在模板内部直接用 T 声明一个未初始化的局部变量,就会触发一个错误(引用对象在创建的时候必须被初始化)
引用包装器 将对象包装成引用包装器 (reference_wrapper
),使其在模板或函数中按引用传递而非值传递,具体有以下两种:
std::ref(x)
→包装成T&
std::cref(x)
→包装成const T&
包装器可以显式的将引用传递给模板函数,避免按值传递。但其也有限制,它不能绑定临时对象,例如std::ref(42)
,需要目标函数/目标模板显式处理reference_wrapper。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <functional> #include <iostream> template <typename T>void process (T arg) { if constexpr (std::is_same_v<T, std::reference_wrapper<int >>) { std::cout << "Reference: " << arg.get () << std::endl; } else { std::cout << "Value: " << arg << std::endl; } } int main () { int x = 42 ; process (x); process (std::ref (x)); }