继续学习现代C++ 模板 C++ 的模板一直是这门语言的一种特殊的艺术,模板甚至可以独立作为一门新的语言来进行使用。模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。因此模板也被很多人视作 C++ 的黑魔法之一。
外部模板 传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化。 为此,C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化:
1 2 template class std ::vector<bool >; extern template class std ::vector<double >;
“>” 在传统 C++ 的编译器中,>>一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:
1 std::vector<std::vector<int >> matrix;
这在传统 C++ 编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译
类型别名模板 在传统 C++ 中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。 C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:
1 2 3 4 5 6 7 8 typedef int (*process) (void *) ;using NewProcess = int (*)(void *);template <typename T>using TrueDarkMagic = MagicType<std::vector<T>, std::string>;int main () { TrueDarkMagic<bool > you; }
变长参数模板 模板一直是 C++ 所独有的黑魔法(一起念:Dark Magic)之一。 在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子, 接受一组固定数量的模板参数;而 C++11 加入了新的表示方法, 允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
1 2 3 4 5 6 7 template <typename ... Ts> class Magic ;class Magic <int , std::vector<int >, std::map<std::string, std::vector<int >>> darkMagic; class Magic <> nothing;template <typename Require, typename ... Args> class Magic ;
除了在模板参数中能使用 … 表示不定长模板参数外, 函数参数也使用同样的表示法代表不定长参数, 这也就为我们简单编写变长参数函数提供了便捷的手段,例如:
1 2 3 4 5 6 template <typename ... Args> void printf (const std::string &str, Args... args) ;template <typename ... Ts>void magic (Ts... args) { std::cout << sizeof ...(args) << std::endl; }
其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:
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 27 28 29 30 31 #include <iostream> template <typename T0>void printf1 (T0 value) { std::cout << value << std::endl; } template <typename T, typename ... Ts>void printf1 (T value, Ts... args) { std::cout << value << std::endl; printf1 (args...); } int main () { printf1 (1 , 2 , "123" , 1.1 ); return 0 ; } template <typename T0, typename ... T>void printf2 (T0 t0, T... t) { std::cout << t0 << std::endl; if constexpr (sizeof ...(t) > 0 ) printf2 (t...) ; } template <typename T, typename ... Ts>auto printf3 (T value, Ts... args) { std::cout << value << std::endl; (void ) std::initializer_list<T>{([&args] { std::cout << args << std::endl; }(), value)...}; }
折叠表达式 1 2 3 4 5 6 7 8 #include <iostream> template <typename ... T>auto sum (T ... t) { return (t + ...); } int main () { std::cout << sum (1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ) << std::endl; }
非类型模板参数推导 1 2 3 4 5 6 7 8 9 10 template <typename T, int BufSize>class buffer_t {public : T& alloc () ; void free (T& item) ; private : T data[BufSize]; } buffer_t <int , 100 > buf;
C++17 引入了这一特性,我们的确可以 auto 关键字,让编译器辅助完成具体类型的推导, 例如:
1 2 3 4 5 6 7 8 template <auto value> void foo () { std::cout << value << std::endl; return ; } int main () { foo <10 >(); }
面向对象 委托构造 C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> class Base {public : int value1; int value2; Base () { value1 = 1 ; } Base (int value) : Base () { value2 = value; } }; int main () { Base b (2 ) ; std::cout << b.value1 << std::endl; std::cout << b.value2 << std::endl; }
继承构造 在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字 using 引入了继承构造函数的概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> class Base {public : int value1; int value2; Base () { value1 = 1 ; } Base (int value) : Base () { value2 = value; } }; class Subclass : public Base {public : using Base::Base; }; int main () { Subclass s (3 ) ; std::cout << s.value1 << std::endl; std::cout << s.value2 << std::endl; }
显式虚函数重载 1 2 3 4 5 6 struct Base { virtual void foo () ; }; struct SubClass : Base { void foo () ; };
SubClass::foo 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。另一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并摇身一变成为了一个普通的类方法,这将造成灾难性的后果。 C++11 引入了 override 和 final 这两个关键字来防止上述情形的发生。
当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的其函数签名一致的虚函数,否则将无法通过编译:
1 2 3 4 5 6 7 struct Base { virtual void foo (int ) ; }; struct SubClass : Base { virtual void foo (int ) override ; virtual void foo (float ) override ; };
final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。
1 2 3 4 5 6 7 8 9 10 11 12 struct Base { virtual void foo () final ; }; struct SubClass1 final : Base {}; struct SubClass2 : SubClass1 {}; struct SubClass3 : Base { void foo () ; };
显式禁用默认函数 在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、 复制构造、赋值算符以及析构函数。 另外,C++ 也为所有类定义了诸如 new delete 这样的运算符。 当程序员有需要时,可以重载这部分函数。 这就引发了一些需求:无法精确控制默认函数的生成行为。 例如禁止类的拷贝时,必须将复制构造函数与赋值算符声明为 private。 尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。 并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。 若用户定义了任何构造函数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。 C++11允许显式的声明采用或拒绝编译器自带的函数。 例如:
1 2 3 4 5 6 class Magic { public : Magic () = default ; Magic& operator =(const Magic&) = delete ; Magic (int magic_number); }
强类型枚举 在传统 C++中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较(虽然编译器给出了检查,但并非所有),甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同,这通常不是我们希望看到的结果。 C++11 引入了枚举类(enumeration class),并使用 enum class 的语法进行声明:
1 2 3 4 5 6 enum class new_enum : unsigned int { value1, value2, value3 = 100 , value4 = 100 };
而我们希望获得枚举值的值时,将必须显式的进行类型转换,不过我们可以通过重载 << 这个算符来进行输出.
1 2 3 4 5 6 7 8 9 10 #include <iostream> template <typename T>std::ostream& operator <<( typename std::enable_if<std::is_enum<T>::value, std::ostream>::type& stream, const T& e) { return stream << static_cast <typename std::underlying_type<T>::type>(e); } std::cout << new_enum::value3 << std::endl
原文链接: https://kettycode.github.io/2024/02/06/cpp/现代C++/base3/
版权声明: 转载请注明出处.