• c++新特性之模板 对象


    目录

    模板

    外部模板

    尖括号

    类别名

    默认模板参数

    变长参数模板

    解包

    折叠表达式

    面向对象

    委托构造

    继承构造

    显式禁用默认函数

    强枚举类型


    模板

    C++ 的模板一直是这门语言的一种特殊的艺术,模板甚至可以独立作为一门新的语言来进行使用。模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。因此模板也被很多人视作 C++ 的黑魔法之

    外部模板

    传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化

    template class std::vector; 强行实例化
    extern template class std::vector; 不在该当前编译文件中实例化模板

    尖括号

    在传统 C++ 的编译器中,>> 一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码

    1. template<bool T>
    2. class MagicType {
    3. bool magic = T;
    4. };
    5. // in main function:
    6. std::vector2)>> magic; // 合法, 但不建议写出这样的代码

    类别名

    在了解类型别名模板之前,需要理解『模板』和『类型』之间的不同。仔细体会这句话:模板是用来产生类型的。在传统 C++ 中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:

    1. template<typename T,typename U>
    2. class MagicType
    3. {
    4. public:
    5. T dark;
    6. U magic;
    7. }
    8. //不合法
    9. template<typename T>
    10. typedef MagicType, std::string> FakeDarkMagic;

    C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:通常我们使用 typedef 定义别名的语法是:typedef 原名称 新名称;,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难

    1. typedef int (*process)(void *);
    2. using NewProcess = int(*)(void *);
    3. template<typename T>
    4. using TrueDarkMagic = MagicType, std::string>;
    5. int main() {
    6. TrueDarkMagic<bool> you;
    7. }

    默认模板参数

    在 C++11 中提供了一种便利,可以指定模板的默认参数:

    1. template<typename T = int, typename U = int>
    2. auto add(T x, U y) -> decltype(x+y) {
    3. return x+y;
    4. }

    变长参数模板

    模板一直是 C++ 所独有的黑魔法(一起念:Dark Magic)之一。在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而 C++11 加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定

    template<typename... Ts> class Magic;
    

    变长参数模板也能被直接调整到到模板函数上。传统 C 中的 printf 函数,虽然也能达成不定个数的形参的调用,但其并非类别安全。而 C++11 除了能定义类别安全的变长参数函数外,还可以使类似printf 的函数能自然地处理非自带类别的对象。除了在模板参数中能使用 … 表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数,这也就为我们简单编写变长参数函数提供了便捷的手段,
    例如:

    1. template<typename... Args>
    2. void printf(const std::string &str, Args... args);

    解包

    1. template<typename... Ts>
    2. void magic(Ts... args)
    3. {
    4. std::cout<<sizeof...(args)<
    5. }
    6. //结果
    7. magic(); // 输出 0
    8. magic(1); // 输出 1
    9. magic(1, ""); // 输出 2

    递归模板参数

    递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参数,进而达到递归遍历所有模板参数的目的

    1. #include
    2. template<typename TO>
    3. void printf(TO value)
    4. {
    5. std::cout<
    6. }
    7. template<typename T, typename... Ts>
    8. void printf1(T value, Ts... args) {
    9. std::cout << value << std::endl;
    10. printf1(args...);
    11. }
    12. int main() {
    13. printf1(1, 2, "123", 1.1);
    14. return 0; }

    你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完成 printf 的编写:

    1. template<typename T0, typename... T>
    2. void printf2(T0 t0, T... t) {
    3. std::cout << t0 << std::endl;
    4. if constexpr (sizeof...(t) > 0) printf2(t...);
    5. }

    初始化列表打开

    递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。

    1. template<typename T, typename... Ts>
    2. auto printf3(T value, Ts... args) {
    3. std::cout << value << std::endl;
    4. (void) std::initializer_list
    5. {
    6. ([&args] {std::cout << args << std::endl;}(), value)...};
    7. }

    通过初始化列表,(lambda 表达式, value)… 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。为了避免编译器警告,我们可以将 std::initializer_list显式的转为 void。

    折叠表达式

    1. #include
    2. template<typename ... T>
    3. auto sum(T ... t) {
    4. return (t + ...);
    5. }
    6. int main() {
    7. std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;
    8. }
    前面我们主要提及的是模板参数的一种形式:类型模板参数。
    1. template<typename T,typename U>
    2. auto add(T t,U u)
    3. {
    4. return t+u;
    5. }
    6. template<typename T,int BufSize>
    7. class buffer_t
    8. {
    9. public:
    10. T& alloc();
    11. void free(T& item);
    12. private:
    13. T data[Bufsize];
    14. }
    15. buffer_t<int,100> buf;//100作为模板参数
    在这种模板参数形式下,我们可以将 100 作为模板的参数进行传递。在 C++11 引入了类型推导这
    一特性后,我们会很自然的问,既然此处的模板参数以具体的字面量进行传递,能否让编译器辅助我们
    进行类型推导, 通过使用占位符 auto 从而不再需要明确指明类型
    幸运的是, C++17 引入了这一特性, 我们的确可以 auto 关键字,让编译器辅助完成具体类型的推导,例如:
    1. template <auto value> void foo() {
    2. std::cout << value << std::endl;
    3. return; }
    4. int main() {
    5. foo<10>(); // value 被推导为 int 类型
    6. }

    面向对象

    委托构造

    C++11 引入了委托构造的概念,这 使得构造函数 可以在同一个类中一个构造函数调用另一个构造函
    ,从而达到简化代码的目的:
    1. #include
    2. class Base{
    3. public:
    4. int value1;
    5. int value2;
    6. Base()
    7. {
    8. value1=1;
    9. }
    10. Base(int value):Base()//委托构造
    11. {
    12. value2=value;
    13. }
    14. };
    15. int main()
    16. {
    17. Base b(2);
    18. std::cout << b.value1 << std::endl;
    19. std::cout << b.value2 << std::endl;
    20. }

    继承构造

    在传统 C++ 中, 构造函数如果需要继承是需要将参数一一传递的 ,这将导致效率低下。 C++11
    用关键字 using 引入了继承构造函数的概念:
    1. #include
    2. class Base {
    3. public:
    4. int value1;
    5. int value2;
    6. Base() {
    7. value1 = 1; }
    8. Base(int value) : Base() { // 委托 Base() 构造函数
    9. value2 = value;
    10. }
    11. };
    12. class Subclass : public Base {
    13. public:
    14. using Base::Base; // 继承构造
    15. };
    16. int main() {
    17. Subclass s(3);
    18. std::cout << s.value1 << std::endl;
    19. std::cout << s.value2 << std::endl;
    20. }
    显式虚函数重载
    在传统 C++ 中,经常容易发生意外重载虚函数的事情。例如:
    1. struct Base {
    2. virtual void foo();
    3. };
    4. struct SubClass: Base {
    5. void foo();
    6. }
    SubClass::foo 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。
    另 一个可能的情形是,当基类的虚函数被删除后, 子类拥有旧的函数就不再重载该虚拟函数并摇身一变成
    为了一个普通的类方法,这将造成灾难性的后果
    C++11 引入了 override final 这两个关键字来防止上述情形的发生。
    override:
    当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函
    数是否存在这样的虚函数,否则将无法通过编译:
    1. struct Base {
    2. virtual void foo(int);
    3. };
    4. struct SubClass: Base {
    5. virtual void foo(int) override; // 合法
    6. virtual void foo(float) override; // 非法, 父类没有此虚函数
    7. };
    final
    final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。
    1. struct Base {
    2. virtual void foo() final;
    3. };
    4. struct SubClass1 final: Base {
    5. }; // 合法
    6. struct SubClass2 : SubClass1 {
    7. }; // 非法, SubClass1 已 final
    8. struct SubClass3: Base {
    9. void foo(); // 非法, foo 已 final
    10. };

    显式禁用默认函数

    传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、复制构造、赋值
    算符以及析构函数。
    另外, C++ 也为所有类定义了诸如 new delete 这样的运算符。当程序员有需要时, 可以重载这部分函数,
    这就引发了一些需求: 无法精确控制默认函数的生成行为
    例如禁止类的拷贝时,必须将复制构造 函数与赋值算符声明为 private。尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。
    并且 编译器产生的默认构造函数与用户定义的构造函数无法同时存在 。若用户定义了任何构造函
    数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数 ,这就造成了尴尬:
    C++11 提供了上述需求的解决方案, 允许显式的声明采用或拒绝编译器自带的函数
    1. class Magic {
    2. public:
    3. Magic() = default; // 显式声明使用编译器生成的构造
    4. Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
    5. Magic(int magic_number);
    6. }

    枚举类型

    1. enum class new_enum : unsigned int {
    2. value1,
    3. value2,
    4. value3 = 100,
    5. value4 = 100
    6. };
    这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数
    字进行比较,更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那
    么可以进行比较:
    1. if (new_enum::value3 == new_enum::value4) {
    2. // 会输出
    3. std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
    4. }
  • 相关阅读:
    【电商营销】了解“客户旅程”,提高客户忠诚度
    盘点 JavaScript 中类的继承
    三生随记——茶叶的诅咒
    ubuntu环境下基于cerbero构建GStreamer及使用VSCode进行调试
    python应用(3):读取excel文件并导出为json文件
    智能合约提款和转账错误
    影响网站排名的4个因素,教你提高网站排名的方法
    Alibaba最新发布的Spring Boot项目实战文档,Github标星78k
    kubernetes的这几种存储卷,别再傻傻分不清了
    长整型(Long Integer)在Python中是一种用于表示大整数的数据类型
  • 原文地址:https://blog.csdn.net/qq_62309585/article/details/126710741