标签: C++

  • [asio]学习笔记2. 概述-异步模型(Executors, Associators)

    前言

    虽然开始尝试自己基于asio写一些东西,同时也在看asio的源码,但真的想学还是应该先看作者本人写的概述:
    https://think-async.com/Asio/asio-1.30.2/doc/asio/overview.html

    这个老哥抽象能力真的很强,这些概念第一遍根本理不清楚,索性开一篇文章专门记录。

    本文记录的内容对应asio概述中这一段的内容:
    file

    其实写asio的时候会觉得很顺,但是细想才会发现这里是有大量的概念交织下才能够实现的表层写的如此顺,而且效率非常之高。

    理解asio的核心就在于理解它最核心的设计概念,但是这些概念真的很抽象,所以我在这里就是先把概念用语言梳理一遍,然后再用具体的例子把所有的概念变成代码里具体的demo,加深理解。

    异步模型

    asio的核心其实不是一个网络库,而是一个异步模型。

    asio将异步操作确立为异步组合的基本构建单元,目前支持回调函数,兼容future、纤程、协程等,也可以在上下文中混合使用这些模型和单元。

    异步操作

    如前文所述,异步操作就是具体的回调、future和协程等,具体来说用户可以在启动异步操作之后继续做其他的事情,它由一个异步操作是由启动函数和完成句柄构成的:

    file

    其中InitFunc是用户可以调用的函数,用于启动一个异步操作;Completion handler是完成句柄。为什么叫完成句柄而不是完成回调呢?

    可以从这个例子来看:

    socket.async_receive(boost::asio::buffer(buffer_), [this](const boost::system::error_code &e, std::size_t bytes) {
        if (!e) {
            // do something
        }
    });
    co_await socket.async_receive(asio::buffer(buffer), asio::use_awaitable);

    这里就启动了两个异步操作:从套接字中读取数据,两个操作分别是异步函数和C++20协程,其中async_receive就是异步操作的启动函数,它的第二个参数就是completion handler。

    对于异步函数这个异步操作而言,完成句柄就是函数本身;但是对于C++20的协程来说,完成句柄是一个asio::use_awaitable占位符,它代表两个意思:

    1. 这是一个C++20协程的占位符
    2. 会返回一个可等待对象

    这也就是完成句柄不叫完成函数的原因,除了定义完成时的行为,还需要根据句柄改变函数本身的返回值,以适应不同的异步操作。
    在这里不展开,有兴趣可以读这一篇文章,看看这种动态是如何实现的。

    异步代理

    这个概念在文档里反复出现和提及,但是因为没有很具体的实体,对我而言有点难理解,但是不理解这个概念后面围绕这个概念的设计就很难吸收。我读了三次才明白这个概念,我先按原文梳理作者的定义,后面再给出我的理解。

    file

    作者抽象了一个新的概念叫做异步代理,它是由多个异步操作通过顺序组合形成的逻辑单元,所以每个异步操作都被视为某个代理的组成部分,如下图所示:
    file

    首先按照作者的定义,异步代理有两个特点:

    1. 组成的异步操作严格串行
    2. 可以和其他的代理并行

    那例如一个socket的所有操作就不能是一个异步代理,因为读和写是可以并行的,那再深入想一下,对于一个socket的所有读操作是可以构成一个异步代理的,对于一个socket的所有写操作也是同理。

    为什么要这么区分呢?因为所有的读操作可以共用同一块内存,所有的写操作也可以公用同一块内存,这个抽象的概念是为了在开发时清晰每个buffer作用的范围,cancel实际断开的异步代理对应的异步操作链条。

    到这里就能更清晰的理解这个概念抽象的目的,异步代理不是藏在asio这个库下层的概念,而是需要深入使用者的脑子里的概念!在用的时候我们需要知道每个异步操作是在哪个代理里,同一个代理的所有操作是严格串行的,它们共用同一块资源。

    那例如我存在一个这样的需求:将某个文件按照csv格式修改一块数据并且存盘,这一整个就是一个异步代理链条,我们需要:

    1. 打开文件
    2. 按照csv格式解析
    3. 读出数据
    4. 修改数据
    5. 写入文件

    这里读写文件可以用同一片内存进行操作,数据不会存在冲突。

    但是例如前面提到的全双工的套接字,它本质上是两条异步代理,这两条异步代理的前面操作是重叠的:
    接收代理:

    1. 建立连接
    2. 接收数据
    3. 解析数据
    4. 回到2继续接受数据

    发送代理:

    1. 建立连接
    2. 等待可发送数据
    3. 发送数据
    4. 回到2继续等待

    在这个例子里,需要两套buffer,一套接收使用,一套发送使用。

    当然,如果是严格的类似于http协议的过程,请求-回复,则一条连接的收发是一个完整的异步代理。

    所以本质上作者在这里提到异步代理是希望开发者在开发时,深入考察是否哪些异步操作可以并行,哪些异步操作之间存在临界区。在抽象出来这个概念之后,一个异步代理的所有操作就像是一个线程里的同步操作。

    异步代理的特指和关联器

    异步代理关联的异步操作具有一些关联性的特征,是需要开发者开发或者处理的,例如:

    1. 分配器,决定异步操作如何获得内存资源
    2. 取消槽,如何支持取消异步操作
    3. 执行器,决定了异步完成代理是如何调度和执行的

    子代理

    file

    如上图所示,这个更像是例如多个异步操作之间存在层的概念的一种封装,类似于HTTP层服务端等待数据抵达,会触发ssl层的数据收发,ssl层又等待tcp层,ssl层在tcp层数据抵达之后可能要等待多次ssl层的握手,才告诉http层,收到数据了。

    这里子代理的概念是类似于这样的实例的抽象,可以将一整个代理封装成一个简单的异步事件。

    执行器

    执行器就是在完成之后决定如何执行完成句柄,对应到代码里就是executor的概念,对应到库里面其实就是io_context, strand这些,在构造socket对象的时候会传入的那个参数。

    内存分配器

    这里的内存分配器是特指库里的一个接口,每个异步操作都会关联一个分配器,用来给异步操作获取每次可稳定操作的内存资源(POSMs)。这个名字本身也反映了两个特质,内存是按每次操作进行分配的(因为内存在该操作的生命周期内保留),并且是稳定的。

    异步操作可以有多种方式来分配POMs,用户可以忽略分配器,使用默认的分配器,也可以根据需要去定制分配器。

    取消

    定时器和套接字都支持close或者cancel取消操作,同时某些异步操作还支持对单次操作。
    为了支持取消操作,需要向代理对象的插槽中注册一个取消处理函数。取消处理函数是当用户发出取消信号时会被触发执行,由于取消插槽和单个代理体绑定,所以同时只支持一个处理程序。
    可以注册覆盖新的函数去覆盖旧函数。

    End.

  • std::map学习笔记

    本来只是想草草读一下这篇文章:https://142857.red/book/stl_map/
    但是还是发现一些值得记录的点,在这里记录一下:
    []读取不存在的key会报错

    map<string, int> config = {
        {"timeout", 985},
        {"delay", 211},
    };
    print(config["timeout"]); // 985
    print(config["tmeout"]);  // 默默返回 0

    推荐所有的读取都用at:

    map<string, int> config = {
        {"timeout", 985},
        {"delay", 211},
    };
    print(config.at("timeout"));  // 985
    print(config.at("tmeout"));   // 该键不存在!响亮地出错

    []会自动创建不存在键值

    map<string, int> config = {
        {"delay", 211},
    };
    config.at("timeout") = 985;  // 键值不存在,报错!
    config["timeout"] = 985;     // 成功创建并写入 985

    因此它引入了一条原则:

    • 读取元素时,统一用 at()
    • 写入元素时,统一用 []

    C++不是python,auto要显示指定引用

    void PeiXunCpp(string stuName) {
        auto stu = stus.at(stuName);  // 这是在栈上拷贝了一份完整的 Student 对象
        stu.money -= 2650;
        stu.skills.insert("C++");
    }

    在python类似的写法是没有问题的,但是在C++中这个stu左边的auto没有显示指定&为引用,则在实际构造的过程中会发生一次拷贝构造函数,并且后续的修改不会贴回到stus的字典里。

    End.

  • 编译clang搞清楚的一些概念(gcc, llvm, clang, libc++, libstdc++)

    clang++/llvm

    因为开发机的gcc版本太低了,就重新编译了一个clang,大概了解了一下这些底层工具的概念。

    clang++和clang都是llvm的一部分,而llvm是一个庞大的、模块化编译器的项目,clang++只是基于llvm的编译器前端。

    所以对于一个C++编译过程,从.cpp生成.o,在llvm的这个项目下的流程是:

    1. clang++对源代码进行词法分析,转化成抽象的AST树;
    2. clang++再将AST转化成中间表示(IR)
    3. 生成IR之后,llvm编译框架对这个中间进行优化;
    4. 接着llvm的后端再将编译后的IR转化成目标机器代码,并生成目标文件(.o)

    所以我们想编译一个完整的clang++,是在llvm项目,因为clang++依赖llvm作为后端,而clang++只是llvm的一种前端。

    libc++

    这个是clang实现的一版C++标准,不同于gnu gcc的实现。
    再编译了之后,我发现还是无法编译C++20的内容,提示找不到<coroutine>,那很自然是库的问题,我们可以在llvm的官网看到项目包含的子项目:
    https://llvm.org/
    除了clang,还有libc++, libc++ABI和libc。

    我重新编译了之后还是有问题,最终是在加了参数-stdlib=libc++之后才能正常编译,并且编译成功之后还是没办法正常的运行,我手动把/usr/local/lib/x86_64-unknown-linux-gnu/目录下的libc++.so.1用软连接放到/usr/lib下才全部搞定。

    标准库的实现

    这里我才知道,原来编译器和标准库是拆开的,编译器是clang,标准库可以用gnu gcc的,也可以用clang自己的libc++,这里列一下C++的标准实现:

    • libstdc++: GNU Compiler Collection (GCC) 提供的;
    • libc++: clang++提供的;
    • Microsoft C++ Standard Library (MSVC STL): 微软vs使用的
    • Dinkumware C++: 常用语嵌入式系统以及商业编译器中。

    参考

    https://developer.huawei.com/consumer/cn/forum/topic/41600287
    https://libcxx.llvm.org/
    https://llvm.org/
    https://llvm.org/docs/CMake.html

    End。

  • 用python理解C++20协程的设计

    C++20拥有一个全新的特性:协程。

    我来从python的角度来解释C++这个特性设计与其他语言的不同,目的以及意义。

    协程是一种可以挂起和恢复执行的函数。C++20协程跟python的生成器是很相似的,如果函数中出现了co_yield, co_return, co_await,那么这个函数就是协程函数。而协程的本质就是将一个函数拆分成多个不能控制执行顺序,但是可以控制执行时机的语言结构。

    对普通函数而言,执行一个函数是开始运行你的逻辑;但是对于协程来说,执行协程函数会先创建一个协程对象,但是是否开始执行还是挂起是视情况而定的。

    例如这个python代码:

    def hello():
        print("Start run")
        ret = yield2
        print("After suspend", ret)
        yield3
        print("After Suspend")
    
    def main():
        generator=hello()
        print(generator)
        ret1 = next(generator)
        print("ret1: ", ret1)
        ret2 = generator.send(23)
        print("ret2: ", ret2)
        generator.send(23)

    在这里hello就是一个协程函数,执行之后会获得一个generator,对generator执行send或者next才会开始真正的执行协程逻辑,从头或者从上次执行的地方一直直行到下一个yield,然后再挂起返回给外面。

    python的生成器跟C++协程很像,都是无栈协程,但是C++的更复杂,因为C++是需要自定义协程对象。
    例如这个简单的C++逻辑:

    TaskCoroutine task_func() {
        std::cout << "task first run" << std::endl;
        co_yield 6;
        std::cout << "---before await task2---" << std::endl;
        co_await task2();
        std::cout << "task resume" << std::endl;
        co_return 3;
    }
    
    int main() {
        std::cout << "Before task_func" << std::endl; 
        TaskCoroutine load_task = task_func();
        std::cout << "After task_func" << std::endl;
        load_task.resume();
        std::cout << "After resume" << std::endl;
        return 0;
    }

    这里因为有co_yield,所以task_func是一个协程函数,但是跟python不同,C++需要定义携程的返回类型,是一个自定义类型,代表的这个携程的Task或者Coroutine。

    这个类型类型必须包含promise_type名字的PromiseType类,例如:

    struct TaskCoroutine
    {
        using promise_type = PromiseType;
    }

    没有这个那编译就会异常,感觉这个是类似于C++20的概念约束做的。

    promise_type又是另外一个约束的类,必须拥有几个必须定义的函数,以及几个可选定义的函数:

    struct PromiseType {
        PromiseType()
        {
            std::cout << "PromiseType" << std::endl;
        }
        TaskCoroutine get_return_object() {
            std::cout << "get_return_object" << std::endl;
            return TaskCoroutine{std::coroutine_handle<PromiseType>::from_promise(*this)};
        }
        std::suspend_never initial_suspend() noexcept {
            std::cout << "initial_suspend" << std::endl;
            return {};
        }
        Awaiter<true> final_suspend() noexcept {
            std::cout << "final_suspend" << std::endl;
            return {};
        }
        void unhandled_exception() {
            std::cout << "unhandled_exception" << std::endl;
        }
        std::suspend_always yield_value(int x) noexcept {
            std::cout << "yield_value" << std::endl;
            return {};
        }
        void return_value(int x) noexcept {
            std::cout << "return_value" << std::endl;
        }
    };

    剩下的内容实在是不想写了,看我的视频吧:
    https://www.bilibili.com/video/BV1H66aYTE84

  • constexpr, consteval, constinit家族

    介绍

    这是《现代C++语言核心特性解析》的读书笔记。

    constexpr家族的内容

    书里给constexpr给了很多篇幅,但是总体来说就是一个,const表示的是不允许修改的常量,而constexpr表示的是编译期的常量。
    但是逐渐被扩展到constexpr可以被用作函数,lambda,以及if constexprt,都是为了让编译期可以做更多的事情,给与开发者更多的自主性。
    反正基本上想要去做一些预计算以及在模板里,用constexpr就没错了。

    consteval是什么呢?

    这件事情我理解来说就是constexpr被扩展定义之后的一个收缩,constexpr最初是为了定义编译期的常量,好让编译器在编译器做一些逻辑。但是逐渐扩展到函数也能放,同时运行期也能调用constexpr的函数。
    那consteval做的事情就是,定义一个函数,这个函数只允许编译期运行。

    说实话,有点过渡定义……

    constinit的构造

    因为之前做一个东西遇到过类似的依赖问题,后来用了宏+一个很取巧的类构造的方式来保证顺序。
    然后C++20提供了一个更简单的方法,constinit保证在编译期就会初始化对象,相当于是最早初始化的一批对象。

    1. 编译期的计算都可以在编译期算好,就不会有启动的依赖关系;
    2. 可以被其他的非constinit对象依赖。

    is_constant_evaluated

    新标准还提供了一个新函数,用于区分当前逻辑运行在编译期还是运行时,可以用if constexpr去做不同的逻辑。

    #include <cmath>
    #include <type_traits>
    constexpr double power(double b, int x) {
      if (std::is_constant_evaluated() && x >= 0) {
        double r = 1.0, p = b;
        unsigned u = (unsigned)x;
        while (u != 0) {
          if (u & 1) r *= p;
          u /= 2;
          p *= p;
        }
        return r;
      } else {
        return std::pow(b, (double)x);
      }
    }
    
    int main() 
    {
      constexpr double kilo = power(10.0, 3);  // 常量求值
      int n = 3;
      double mucho = power(10.0, n);           // 非常量求值
      return 0;
    }
  • 结构化绑定(C++17, C++20) 学习笔记

    结构化绑定

    在python中是能够很轻易地实现多个返回值的函数的,例如:

    def return_multi_values():
      return 100, 20
    x, y = return_multi_values()

    但是在C++应该是会用类、结构体或者引用来实现,在C++11之后也引入了std::tuple,也可以用来实现类似的功能:

    #include <iostream>
    #include <tuple>
    
    std::tuple<int, int> return_multiple_values()
    {
      return std::make_tuple(11, 7);
    }
    
    int main()
    {
      int x = 0, y = 0;
      std::tie(x, y) = return_multiple_values();
      std::cout << "x=" << x << " y=" << y << std::endl;
    }

    这里定义和获取都很麻烦:

    1. 定义的时候需要显示指定返回类型;
    2. 同时在用std::tie解邦定前需要先声明x, y的变量。
      第一个问题可以通过auto来实现:

      auto return_multiple_values()
      {
      return std::make_tuple(11, 7);
      }

      第二个问题就需要使用C++17引入的新特性——结构化绑定。

    所谓结构化绑定是指将一个或多个名称绑定到初始化对象中的一个或多个子对象(或者元素)上,相当于给初始化对象的子对象起了别名。这里引用和别名是有区别的。
    使用结构化绑定的方式是 auto[xx, yy, zz] = xxxx,例如:

    #include <iostream>
    #include <tuple>
    
    auto return_multiple_values()
    {
      return std::make_tuple(11, 7);
    }
    
    int main()
    {
      auto[x, y] = return_multiple_values();
      std::cout << "x=" << x << " y=" << y << std::endl;
    }

    但右边的值也不一定非得是函数返回值或者tuple,合理的表达式也行:

    #include <iostream>
    #include <string>
    
    struct BindTest {
      int a = 42;
      std::string b = "hello structured binding";
    };
    
    int main()
    {
      BindTest bt;
      auto[x, y] = bt;
      std::cout << "x=" << x << " y=" << y << std::endl;
    }

    在for循环里会有更好的效果:

    #include <iostream>
    #include <string>
    #include <vector>
    
    struct BindTest {
      int a = 42;
      std::string b = "hello structured binding";
    };
    
    int main()
    {
      std::vector<BindTest> bt{ {11, "hello"},  {7, "c++"},  {42, "world"} };
      for (const auto& [x, y] : bt) {
           std::cout << "x=" << x << " y=" << y << std::endl;
      }
    }

    深入理解

    这两个理解是错误的:

    1. 结构化绑定的目标就是等号右边的对象
    2. 所谓的别名就是对等号右边对象的子对象或者元素的引用。

    在结构化绑定中编译器会根据限定符生成一个等号右边对象的匿名副本,而绑定的对象正是这个副本而非原对象本身。
    auto [xx, yy, zz]是能够增加修饰符的,例如const,volatile等, 甚至&引用也可以,这些都会直接作用在新的变量上。

    结构化绑定3种类型

    包括原生数组、结构体和类对象、元组和类元组的对象。

    绑定原生数组

    它是所有情况中最简单的,条件是别名数量和数组数量一致。

    绑定到结构体和类对象

    首先,类或者结构体中的非静态数据成员个数必须和标识符列表中的别名的个数相同;其次,这些数据成员必须是公有的(C++20标准修改了此项规则,详情见20.5节);这些数据成员必须是在同一个类或者基类中;最后,绑定的类和结构体中不能存在匿名联合体

    绑定到元祖和类元祖对象

    类元祖对象的定义比较复杂,不像python有ABC这种元类,C++是通过模板实现判断,目标类型提供std::tuple_size、std::tuple_element以及get的特化或者偏特化版本即算是类元祖对象。

    在标准库中,除了std::tuple,还有std::pair和std::array也满足这个条件,这就带来了一个很好的特性:能够在for循环遍历map的时候直接使用结构化绑定,例如:

    #include <iostream>
    #include <string>
    #include <map>
    
    int main()
    {
      std::map<int, std::string> id2str{ {1, "hello"}, {3, "Structured"}, {5, "bindings"} };
    
      for (const auto& elem : id2str) {
           std::cout << "id=" << elem.first
                << ", str=" << elem.second << std::endl;
      }
    }

    End.

  • override和final说明符(C++11) 学习笔记

    重写、重载和隐藏

    重写(override)、重载(overload)和隐藏(overwrite)在C++完全不同的概念,先梳理一下区别:

    重写

    重写的意思更加接近于覆盖,在C++中是指派生类覆盖了基类的虚函数,这里的覆盖必须满足有相同的函数签名和返回类型,即重写是有相同的函数名、形参列表以及返回类型。

    重载

    它通常指一个类中有两个或者以上的函数,他们函数名相同,但是函数签名不同。

    隐藏

    隐藏的概念是指基类成员函数,无论是否是虚函数,当派生类出现同名函数时,如果派生类函数签名和基类不同,则基类的会被隐藏;如果派生类函数签名和基类相同,如果是虚函数则为重写,否则为隐藏。

    如果想在子类中使用基类的函数,可以使用using关键字将其引入派生类。

    override: 重写的问题

    重写容易出现问题,即基类定义了虚函数,但是子类的函数名写错了,也不会有编译错误,只有运行测试时才会发现错误。

    因此C++引入了一个非常实用的关键字,即override,这个关键词告诉编译器,这个函数需要覆盖基类的虚函数,一旦编译器发现虚函数不符合重写规则,就会报错。

    final说明符

    在C++引入了final关键词来阻止派生类继承虚函数。它告诉编译器,这个函数不能被重写,如果重写了会编译报错,跟override一样放在函数声明的尾部。

    End.

  • C++ 11/17强枚举类型读书笔记

    强枚举类型

    原来继承自C语言的枚举类型在C++之父看来是一个奇怪且半生不熟的概念。

    枚举的弊端

    虽然枚举类型可以避免A类型赋值给B类型,但是:

    • 可以直接跨枚举比较
    • 可以直接转化为int
    • 同名的枚举值是冲突的

    虽然有很多缺点,但依然是建议使用枚举而不是const int来做枚举,那样问题只会更多。

    强枚举类型

    C++11标准增加了强枚举类型,为了保证老代码的兼容性,同时也兼容了旧的特性,新增的枚举类型具有三个特性:

    • 枚举标识符属于强枚举类型的作用域。
    • 枚举标识符不会隐式转换为整型。
    • 能指定强枚举类型的底层类型,底层类型默认为int类型。
      基本上就是让枚举不是一个int的别名,而是一个独立的完全定义,具备完全语义的类型,同时解决了枚举值作用域的问题。

    为了兼容旧的逻辑,所以使用了新的标识符,从enum替换成了enum class。

    列表初始化有底层类型枚举对象

    这一段真的很难理解,为什么C++标准要搞这种东西,即使书里面有一些解释,我还是觉得不太理解…
    首先强枚举类型支持由int作为参数的列表初始化:

    enum class Color {
     Red,
     Green,
     Blue
    };
    int main()
    {
     Color c{5};
     Color c1 = 5;
     Color c2 = {5};
     Color c3(5);
    };

    这个例子真的震惊到我,Color的范围不是[0~2]吗,怎么就能赋值成5?
    而且{5}和(5)的差别是列表初始化构造函数和参数构造函数,为什么要有这种奇怪的特性?C++真的越来越折磨人。。。
    说是为了定义一种特殊的整数类型,同时这个整数类型不能跟其他的整数互相转换,强枚举类型符合这个特性,所以通过列表初始化构造函数让强枚举类型变成了一种非通用整数类型的整数类型。
    C++真的越来越折磨人。。。

    用using打开强枚举类型

    可以使用using namesapce;的方式,省略掉强枚举类型的前缀,直接在上下文使用枚举值。

    enum class Color {
     Red,
     Green,
     Blue
    }
    const char* ColorToString(Color c)
    {
     switch(c)
     {
      case Color::Red: return"Red";
      case Color::Green: return"Green”
      case Color::Blue: return"Blue";
      default:
        return"none";
      }
    }

    可以改成:

    enum class Color {
     Red,
     Green,
     Blue
    }
    const char* ColorToString(Color c)
    {
     switch(c)
     {
      using Color;
      case Red: return"Red";
      case Green: return"Green”
      case Blue: return"Blue";
      default:
        return"none";
      }
    }

    End.

  • 默认和删除函数(C++11)笔记

    类的特殊成员函数

    在C++中定义一个类,会默认生成以下6个成员函数:

    • 默认构造函数
    • 析构函数
    • 复制构造函数
    • 复制赋值运算符函数
    • 移动构造函数(C++11 新增)
    • 移动赋值运算符函数(C++11 新增)

    这些功能很实用,在我们直接定义一个类之后,能够直接互相拷贝,而不用为了编译器语法的原因一定要写一遍这些函数。

    但是它也有一些潜规则:

    • 声明任何构造函数都会抑制默认构造函数的添加。
    • 一般用自定义的构造函数替代默认构造函数,类就会转化为非平凡类型。

    非平凡类

    什么是平凡类什么是非平凡类呢?
    这里的概念是出自C语言,C语言想要复制一个对象最快速的方法是直接把整个对象的内存从a复制到b,而不用考虑其他的情况。

    但是对C++的类,可能就不能通过直接复制对象的内存来实现复制,因为类可能定义了构造函数、复制构造函数,要在直接的内存copy之前或者之后做一些逻辑,甚至可能是很重的逻辑,那么对象的拷贝就只能通过复制构造函数来做了。

    因此,平凡类和非平凡类的最大差别是:是否能直接copy类的内存块来实现完整的复制,想做到这个要求这个类在构造的时候和复制的时候不需要做额外的逻辑,但是编译器无法确认你是否有做了额外的逻辑,所以编译器只能定义,显式(explict)定义了四种函数:

    • 构造函数
    • 复制构造函数
    • 复制赋值函数
    • 析构函数
      有另外定义就是非平凡类,但是如果定义了使用=default,也算是默认的,所以不会改变平凡类的性质。

    从我这个角度去分析这个问题应该会简单一点。

    POD是指完全跟C语言的struct兼容,需要是平凡类和标准布局,这俩分别对应的是行为定义和内存定义。

    显示默认和显示删除

    C++11标准提供了一种简单的方法能够有效地控制默认特殊函数的添加和删除,语法很简单,就是在尾部添加=default和=delete。

    这里=default可以在.cpp中去制定,但是=delete必须在.h中指定,不过这个也很好理解。

    显示删除除了在这些地方,还可以使用在普通函数上,可能是给一些库使用,保留旧的函数签名,但是链接新版本时会失败,虽然听起来还是没什么用。

    然后就是可以显式的删除new函数和析构函数,而且这两者的表现完全相反,new操作符被删除之后,就不能通过new创建了,只能通过自动变量、静态变量等方法创建;而析构函数删除后,则无法调用delete函数,同时也无法从自动变量、全局变量中创建。

    参考了一下维基百科里C++的new,原来C++ new一个过程是先用operator new创建内存,然后调用构造函数再返回指针,而new 操作还支持在已经有的内存上直接new一个对象……

    参考

    https://zh.wikipedia.org/wiki/POD_(%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)
    https://zh.wikipedia.org/wiki/New_(C%2B%2B)

    End。

  • Hazel视频笔记 – EventSystem

    介绍

    最近开始听Youtube上一个大佬自研引擎的开发全过程,本篇是我做的笔记。

    事件集中定义

    这篇视频介绍了他对于EventSystem的初步规划和开发,首先我因为用python很多,很习惯于不提前把一切事件都定义好,EventSystem就应该是外部可以定义Event,并且可以发布Event。

    但是他这里做的事情是,定义好了所有的Event,有好有坏,好处呢是所有的事件集中在Event.h的里面,但也有坏处的,坏处就是如果我想增加事件必须要修改引擎的代码,或者要新开发一个Delegate的系统。

    Category是位运算

    博主说为了可以快速区分这个事件是不是一个鼠标事件,就将所有的鼠标事件都集中在同一个Category里,感觉是一个不够抽象的设计。

    如果是我来做,我应该会定义一个Category的抽象类,然后把所有的EventType都放进去,效率可能会比博主的这个方案低一点,但是在外部就可以自由的定义哪些组事件放在一起,个人感觉会比直接定死一批Category要好。