我的C++规范 - 回调的设想

回调的设想

回调函数

main.cpp
复制代码
#include <iostream>
#include <functional>
#include <fstream>

#include "mclog.h"

// 写入回调
struct write_file
{
    // 普通函数
    write_file(const std::string &file, void (*fn_write)(std::ofstream &))
    {
        std::ofstream fs(file);
        if (fs.is_open())
        {
            fn_write(fs);
            fs.close();
        }
    }
};

// 读取回调
struct read_file
{
    // function 类型
    read_file(const std::string &file, std::function<void(const std::string &content)> fn_read = nullptr)
    {
        std::string ret;
        std::ifstream fs(file);
        if (fs.is_open())
        {
            std::string buff;
            while (std::getline(fs, buff))
            {
                ret += buff + "\n";
            }
            if (fn_read)
            {
                fn_read(ret);
            }
            fs.close();
        }
    }
};

// 回调的处理函数
void fun_write(std::ofstream &fs)
{
    fs << "食材:猪肉,牛肉,辣椒,白菜,豆腐" << std::endl;
    fs << "配料:酱油,沙姜,葱花,大蒜" << std::endl;
    fs << "主食:大米,面条,馒头" << std::endl;
    MCLOG("写入函数触发");
}

// 回调的处理函数
void fun_read(const std::string &content)
{
    MCLOG("普通函数读取\n" $(content))
}

int main(int argc, char **argv)
{
    std::string filename = "food_list.txt";

    // 回调写入-使用普通函数处理
    write_file(filename, fun_write);

    // 回调读取-使用普通函数处理
    read_file(filename, fun_read);

    // 回调读取-使用匿名函数处理(lambda)
    read_file(filename, [=](const std::string &content) { 
        MCLOG("匿名函数读取\n" $(content)) 
        MCLOG("文件名为: " $(filename)); 
    });

    // lambda 生命周期
    MCLOG("生命周期");
    std::function<void()> fn_copy = nullptr;
    std::function<void()> fn_ref = nullptr;
    {
        int value = 100;
        int *pvalue = &value;
        fn_copy = [=]()
        {
            MCLOG($(value));
        };
        fn_ref = [&]()
        {
            MCLOG($(value));
        };

        MCLOG("销毁前执行");
        fn_copy();
        fn_ref();

        // 模拟为销毁后的数据改变
        // value 被销毁,被销毁的值是未知的,未定义行为
        *pvalue = 200;
    }
    MCLOG("销毁后执行");
    fn_copy();
    fn_ref();

    return 0;
}
打印结果
复制代码
写入函数触发 [/home/red/open/github/mcpp/example/15/main.cpp:52]
普通函数读取
[content: 食材:猪肉,牛肉,辣椒,白菜,豆腐
配料:酱油,沙姜,葱花,大蒜
主食:大米,面条,馒头
]  [/home/red/open/github/mcpp/example/15/main.cpp:58]
匿名函数读取
[content: 食材:猪肉,牛肉,辣椒,白菜,豆腐
配料:酱油,沙姜,葱花,大蒜
主食:大米,面条,馒头
]  [/home/red/open/github/mcpp/example/15/main.cpp:74]
文件名为: [filename: food_list.txt]  [/home/red/open/github/mcpp/example/15/main.cpp:75]
生命周期 [/home/red/open/github/mcpp/example/15/main.cpp:83]
销毁前执行 [/home/red/open/github/mcpp/example/15/main.cpp:94]
[value: 100]  [/home/red/open/github/mcpp/example/15/main.cpp:88]
[value: 100]  [/home/red/open/github/mcpp/example/15/main.cpp:91]
销毁后执行 [/home/red/open/github/mcpp/example/15/main.cpp:102]
[value: 100]  [/home/red/open/github/mcpp/example/15/main.cpp:88]
[value: 200]  [/home/red/open/github/mcpp/example/15/main.cpp:91]

回调函数,一种对未来可能的预设,你会预设别人会用什么,然后预设你所提供的内容是别人想要的,这只是回调的简单用法之一

回调函数通常是为了将不同的功能分开编写,然后在你的函数里面使用回调函数去处理别人的功能

回调函数通常跟异步逻辑有关,但现在还为涉及到异步相关内容,这部分在后续文章内补充

回调函数

在 main.cpp 文件中,我先写入一段内容到文件,但是我没有直接通过 ofstream 对象写入,而是创建了 write_file 类,在类的构造函数中存在 fn_write 变量,通过这个变量写入内容,然后就关闭文件

这个 fn_write 变量是函数指针,这个变量存储的是函数地址,函数指针可以像普通函数一样去执行,也可以像普遍变量一样当作参数传递,fn_write 函数指针被当成变量来使用,这个被调用的函数就被称为回调函数

在 write_file 构造函数中,请注意 fn_write 函数指针可接收的函数类型,传入的函数需要与 fn_write 的函数类型保持一致才行,在调用时传入的是 fun_write 函数和 fn_write 的函数声明是一样的,声明包括返回值,参数个数,参数类型

当传入 fun_write 函数之后,fn_write 函数指针就指向 fun_write 函数,等于绑定了关系,执行 fn_write 就等于执行 fun_write ,这一点应该不难明白

函数的可替换性

回调函数的意义在于可以被随时替换掉,正执行的是函数指针,而不是具体的函数,原因就在于函数指针是可以改变指向的

在 read_file 类中,我读取了两遍文件,但是他们的打印是不一样的,因为他们指向的不是同一个函数,这就是通过传入不一样的函数实现不一样的功能,其实这一点和面向对象的多态效果是一致的,你可以在调用时动态的决定代码的走向

回调的细节

通过 main.cpp 代码你可能已经发现,我使用了保护普通函数,匿名函数(lambda),函数指针,std::function 类对象这几种方式混合编写了回调函数,其实我只想告诉你可以这么干

实际上我只推荐你使用 std::function 和 lambda 编写回调,使用 lambda 的意义在于每一次回调都是可能不一样的,而且每一次你都可以随时调整你的回调执行任务

相比与传统的函数指针,std::function 在使用上更像一种变量,它可以很明确的声明一种函数类型,然后在后续中调用,我推荐你使用 std::function ,它可以代替函数指针,但是你要注意的是, std::function 是类对象,所以建议在执行时判空,否则传入为空时执行会直接崩溃

回调的复用性

其实编写回调主要有两个用途,一个是网络的异步编程,一个是功能复用

你可以从 read_file 两个类中看出,打开文件需要执行文件三部曲,然后在读取全部内容到字符串,使用回调可以在成功读取内容之后执行回调任务,如果打开失败我就不处理了,如果是使用普通函数处理方式,我需要返回打开成功或者失败,在外部判断是否成功或者失败才能对字符串进行处理

那如果我需要处理失败,使用回调的方式就是在传入一个错误回调函数即可

lambda传参
复制代码
std::function<void()> fn_copy = nullptr;
std::function<void()> fn_ref = nullptr;
{
    int value = 100;
    int *pvalue = &value;
    fn_copy = [=]()
    {
        MCLOG($(value));
    };
    fn_ref = [&]()
    {
        MCLOG($(value));
    };

    MCLOG("销毁前执行");
    fn_copy();
    fn_ref();

    // 模拟为销毁后的数据改变
    // value 被销毁,被销毁的值是未知的,未定义行为
    *pvalue = 200;
}
MCLOG("销毁后执行");
fn_copy();
fn_ref();

如果你已经学习过 lambda 就会发现,匿名函数可以获取到当前作用域的变量数据,但是当你使用这些变量时,这个匿名函数总是莫名其妙的崩溃

这可能是你没有了解参数的声明周期导致的,使用 [=] 是拷贝行为, [&] 是引用行为,拷贝就是自己存在一份副本,而引用则使用原来的数据

当使用引用时,一旦引用的对象声明周期结束,你的程序获取的数据就是未定义的,可能是原来的值,也可能是乱码,所以就会引发崩溃

可以看到 fn_copy fn_ref 在变量销毁后,fn_copy 可以打印原来的值,而 fn_ref 是不确定的

lambda 使用变量时的周期中需要注意,使用拷贝时几乎就和外部没关系了,因为会拷贝出另一块新内存地址,但是要保证数据没有指针,否则指针被释放一样会崩溃

使用引用时,你需要关注使用的数据不能提起被释放,否则数据失效,因为引用的是同一块内存,通常不推荐使用引用类型的参数,因为相比拷贝,使用引用需要注意的问题更多,而且容易疏忽,所以更容易崩溃

项目路径

复制代码
https://github.com/HellowAmy/mcpp.git
相关推荐
Dreamy smile2 小时前
JavaScript 继承与 this 指向操作详解
开发语言·javascript·原型模式
副露のmagic2 小时前
更弱智的算法学习 day53
开发语言·python
Java程序员威哥2 小时前
SpringBoot多环境配置实战:从基础用法到源码解析与生产避坑
java·开发语言·网络·spring boot·后端·python·spring
mudtools2 小时前
C#中基于Word COM组件的数学公式排版实践
开发语言·c#·word
安全二次方security²2 小时前
CUDA C++编程指南(7.1)——C++语言扩展之函数执行空间指定符
c++·人工智能·nvidia·cuda·cuda编程·global·函数执行空间指定符
Q741_1472 小时前
C++ 优先级队列 大小堆 模拟 力扣 1046. 最后一块石头的重量 每日一题
开发语言·c++·算法·leetcode·优先级队列·
一个处女座的程序猿O(∩_∩)O2 小时前
Next.js 与 React 深度解析:为什么选择 Next.js?
开发语言·javascript·react.js
KiefaC2 小时前
【C++】特殊类设计
开发语言·c++
坐怀不乱杯魂2 小时前
Linux - 进程信号
linux·c++