• 首页
  • 加入
  • RSS
  • Friday, September 16, 2022

    可执行程序

    判断某个可执行程序是否存在

    不推荐做法: 根据硬编码路径判断某个文件是否存在。比如判断 QFile().exists(/usr/bin/Foo)

    推荐做法:根据 PATH 寻找可执行文件, 一般不需要自行读取 PATH,比如 QT 可以使用:

    Exist = !QStandardPaths::findExecutable("Foo").isEmpty();
    

    比如 glib 可以使用 find_program_in_path。

    修改示例:

    执行某个可执行程序

    同样,推荐用 PATH 寻找,尽量不使用绝对路径。

    Qt 的 QProcess 会自动处理 PATH 环境变量,因此 QProcess::execute("/usr/bin/touch", QStringList() << sessionCacheFile) 可以直接改成 QProcess::execute("touch", QStringList() << sessionCacheFile)

    修改示例:

    动态库路径

    尽量不要硬编码 /urs/lib

    qt 应用可以使用 QLibraryInfo::location(QLibraryInfo::LibrariesPath)

    头文件

    绝对不要有 #include </usr/include/xxx.h> 这种代码,c/c++ 头文件会自动在 /usr/include 里寻找,直接使用 #include <xxx.h>

    此外,如果库提供了 pkg-config 文件,提供的 -I 参数会指定头文件位置,如果提供 Config.cmake (cmake用)文件,一般会提供 Foo_INCLUDE_DIR 变量,如果提供 .pri (qmake 用)文件,由 QT.foo.includes 提供头文件位置。xxx.h 是相对提供的路径寻找。

    对于没有提供任何开发文件的库,也可以使用 cmake 提供的 CheckIncludeFile 模块寻找头文件。

    应用数据(/use/share)

    /usr/share,一般保存与架构无关的只读数据。此目录同样不应该硬编码,在 XDG 规范中,这种数据的目录使用环境变量 XDG_DATA_DIRS 设置,如果读不到对应环境变量再去读取 /usr/local/share:/usr/share。

    使用 qt 的应用该类型硬编码路径应该使用 QStandardPaths 或者 libqtxdg 代替。

    参考:XDG Base Directory

    在此目录寻找某文件,推荐使用 QStandardPaths::standardLocations 参考修改:

    /usr/share/applications

    直接使用 QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)

    相关修改:

    /usr/share/AppName

    读取本应用数据,qt 软件可以使用 QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation)

    相关修改:

    导入翻译

    部分应用导入翻译使用了 /usr/share 示例, 应该改为使用 QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation), 此外使用了 dtk 的应用是直接用 DApplication 的 loadTranslator 导入翻译。

    Shebangs

    #!/bin/bash -> #!/usr/bin/env bash
    #!/usr/bin/python -> #!/usr/bin/env python
    

    并非所有系统都有 /bin 目录和 /usr/bin 目录,比如 NixOS 和 GNU Guix 操作系统。 env 可以根据环境变量 $PATH 寻找可执行程序(如 bash),比硬编码路径更加灵活,可以提高可移植性。

    不过并非所有脚本都适合使用 env, 有 2 种情况:

    • 对安全性要求高的脚本(如以 root 权限运行),硬编码可以避免通过相关 PATH 让 bash 指向恶意程序。
    • 带有参数的 Shebangs 头,如 #!/usr/bin/perl -w 不能改成 #!/usr/bin/env perl -w

    对除此之外的大部分的 Shebangs 脚本来说,使用 env 是更好的方案。

    参考资料:

    相关 issues:

    使用 GNUInstallDirs.cmake模块

    在指定安装路径时,应当使用变量而非写死安装目录,以便于在不完全符合 FHS 的系统上安装,提高程序的可移植性。对于使用何种变量, GUN 提出了适用于 unix 系统的 GNU标准安装目录,GNU/Linux 上使用的就是这套标准的变体。cmake 官方提供了 GNUInstallDirs 模块,定义了一组标准的变量,用于安装不同类型文件到规范指定的目录中。

    要使用这个模块,在 CMakeLists.txt 添加一行 include(GNUInstallDirs) 即可导入。如果你发现 CMAKE_INSTALL_XXXX 的值为空,大概率是缺少这一行。注意导入模块需要放在使用变量之前。

    前缀值 CMAKE_INSTALL_PREFIX

    CMAKE_INSTALL_PREFIX(后面简称 PREFIX) 是一个非常特殊的变量,在 CMakeLists.txt 中所有的相对路径都会默认把 PREFIX 作为前缀进行拼接,组成绝对路径。这一变量是 cmake 基础变量,不导入 GNUInstallDirs 模块也会存在。

    举一个例子,假如之前要把文件安装进 /usr/share 目录

    install(FILES ${QM_FILES} DESTINATION  /usr/share/deepin-calculator/translations)
    

    如果设置了 PREFIX=/usr,只需要改成相对路径 share,cmake 会自动拼接成 /usr/share

    install(FILES ${QM_FILES} DESTINATION  share/deepin-calculator/translations)
    

    这样写,如果安装前缀改变,比如改成 /usr/local,只需要修改 CMAKE_INSTALL_PREFIX,而不用改 CMakeLists.txt 源码。

    目前 CMAKE_INSTALL_PREFIX 按 GNU 的标准默认值是 /usr/local。而在 dde 的项目中,一般期望前缀值是 /usr。我们当然可以通过 cmake -DCMAKE_INSTALL_PREFIX=/usr 来修改 PREFIX,但最好在项目中添加下面 3 行修改一下 PREFIX 的默认值:

    if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
        set(CMAKE_INSTALL_PREFIX /usr)
    endif ()
    

    这里容易犯两种错误:

    1. 没有检查 CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT 就直接 set:这会导致用户传入的参数失效,相当于硬编码了路径。
    2. 使用 if (NOT DEFINE CMAKE_INSTALL_PREFIX ) 判断用户是否传参了 PREFIX:实际上 PREFIX 无论什么情况都是有定义的, 只能使用 CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT 判断是否是默认值。

    对 CMAKE_INSTALL_PREFIX 的配置会对子目录生效,在 cmake 3.14 之前,先应用安装规则再处理 add_subdirectory,而 3.14 及后续版本,会按照声明的顺序运行所有安装规则/add_subdirectory,因此,除非你想给子目录单独配置,上述的设置应写在 add_subdirectory 的前面。 见 CMP0082

    GNUInstallDirs 变量

    在 GNUInstallDirs 中,定义了一些 GNU 标准安装目录的变量,提供给定类型文件的安装路径。这些值可以传递给对应 install() 命令的 DESTINATION 选项。它们通常是相对于安装前缀(PREFIX)的相对路径,以便于以可重定位的方式将其拼接为绝对路径。当然,它们也允许赋值为绝对路径。

    CMAKE_INSTALL_XXXX

    • CMAKE_INSTALL_BINDIR 用户可执行程序( bin

    • CMAKE_INSTALL_SBINDIR 系统管理员可执行程序( sbin

    • CMAKE_INSTALL_LIBEXECDIR 可执行库文件( libexec

    • CMAKE_INSTALL_SYSCONFDIR 单机只读数据/read-only single-machine data( etc

    • CMAKE_INSTALL_SHAREDSTATEDIR 架构无关的可修改数据/modifiable architecture-independent data ( com

    • CMAKE_INSTALL_LOCALSTATEDIR 单机可修改数据/modifiable single-machine data( var

    • CMAKE_INSTALL_RUNSTATEDIR 3.9版中加入:运行时可修改数据( LOCALSTATEDIR/run

    • CMAKE_INSTALL_LIBDIR 目标代码库/object code libraries ( liblib64 ) (在 Debian 上当 PREFIX 是 /usr 时,LIBDIR 也可能是 lib/<multiarch-tuple>

    • CMAKE_INSTALL_INCLUDEDIR C语言头文件( include

    • CMAKE_INSTALL_OLDINCLUDEDIR non-gcc 的C语言头文件( /usr/include

    • CMAKE_INSTALL_DATAROOTDIR 与架构无关的只读数据根目录( share

    • CMAKE_INSTALL_DATADIR 与架构无关的只读数据( DATAROOTDIR

    • CMAKE_INSTALL_INFODIR info 文档( DATAROOTDIR/info

    • CMAKE_INSTALL_LOCALEDIR 与语言相关的数据/locale-dependent data( DATAROOTDIR/locale

    • CMAKE_INSTALL_MANDIR man 文档( DATAROOTDIR/man

    • CMAKE_INSTALL_DOCDIR 文档根目录( DATAROOTDIR/doc/PROJECT_NAME

    需要注意的是 DATAROOTDIR 是 DATADIR,LOCALEDIR,MANDIR 和 DOCDIR 共同前缀,不应该在 install 中直接使用 DATAROOTDIR 作为参数,而是应该使用 DATADIR 代替 。类似 /usr/share/man 的路径应该用 MANDIR 代替,而不是 DATADIR/man

    CMAKE_INSTALL_FULL_XXXX

    CMAKE_INSTALL_BINDIR 推荐用相对路径,但也可以使用绝对路径,有些时候我们需要绝对路径(比如生成 pkgconfig 文件时),直接加 PREFIX 拼接并不好(因为 BINDIR 可能已经是绝对路径了),这时,我们可以直接使用 CMAKE_INSTALL_FULL_BINDIR,这是由 GNUInstallDirs 模块提供,自动从 BINDIR 计算出的绝对路径。如果 BINDIR 是绝对路径,直接相等。如果 BINDIR 是相对路径,则等于 BINDIR 按照一定规则与 PREFIX 拼接而成的值。

    用例: https://github.com/linuxdeepin/dde-dock/pull/556

    当然,install 是没有必要用 CMAKE_INSTALL_FULL_XXXX 的(当然用也可以),因为它会自动判断,并拼接相对路径。

    前缀拼接的特殊情况

    GNUInstallDirs 前缀拼接存在一些特殊情况需要注意:

    1. CMAKE_INSTALL_PREFIX=/

    除了 SYSCONFDIR, LOCALSTATEDIRRUNSTATEDIR 正常拼接外, 其他值都会增加一个 usr/ 前缀 。比如 INCLUDEDIR 默认值 include ,变成 usr/include, 最终拼接成 /usr/include。

    这里自动增加 usr 是符合 GNU 目录标准的,因为这些路径是符号链接。
    ~ ❯❯❯ readlink /lib
    usr/lib
    ~ ❯❯❯ readlink /bin
    usr/bin
    

    2. CMAKE_INSTALL_PREFIX=/usr

    SYSCONFDIR, LOCALSTATEDIRRUNSTATEDIR 拼接时只拼接 “/”。 比如, SYSCONFDIR 默认值 etc 会拼接成 /etc。

    3. CMAKE_INSTALL_PREFIX=/opt/…

    SYSCONFDIR, LOCALSTATEDIRRUNSTATEDIR 会向后拼接。 比如, SYSCONFDIR 会变成 /etc/opt/….

    参考文档

    Sunday, August 14, 2022

    DDE 控制中心自启动管理插件可单独编译、集成至 DDE 控制中心、集中展示并管理Deepin系统中软件开机自启动设置。

    参赛信息

    本项目参加2023全国大学生计算机系统能力大赛操作系统设计赛-功能挑战赛,选题为proj223-control-center-startup-management-plugin

    参赛小组:

    • 复旦大学 朱元依
    • 复旦大学 沈扬
    • 复旦大学 朱俊杰

    指导老师:

    • 复旦大学 张亮
    • 复旦大学 陈辰
    • 企业导师 王子冲

    完成情况

    1、DDE 控制中心自启动管理插件

    任务完成情况
    (必须)完成一个控制中心插件,能够展示当前所有开机启动项的列表完成
    (必须)插件以单独的仓库提供,并能够单独构建,不需要合并入 dde-control-center 项目完成
    (必须)能够在插件中,通过用户界面的交互来管理(添加、删除、启用、禁用)开机启动项完成
    (必须)编写博客,记录开发过程的心得与体会,并将博客投递至 planet.deepin.org完成博客编写,待deepin官方审核

    成果展示

    功能展示视频链接:

    链接:https://pan.baidu.com/s/1YgvhbZ8pypE4CFhuBYw3eg 提取码:rwcc

    插件图标:

    前端页面

    自启动管理窗口:

    自启动管理页面

    添加自启动应用选择界面:

    应用选择页面

    部署方式

    1、系统环境

    开发环境:Deepin V23Beta版

    系统架构:x86

    镜像下载链接:https://mirrors.ustc.edu.cn/deepin-cd/releases/23-Beta/

    虚拟机平台:WMware Workstation 16Pro

    操作系统环境搭建参考博客:https://blog.csdn.net/qq_44133136/article/details/105887560

    2、配置 Deepin 插件环境

    安装依赖包:

    sudo apt build-dep .
    sudo apt install -y qt5-default
    sudo apt-get install dde-control-center-dev
    

    3、安装插件

    sudo sh install.sh
    

    安装成功后,打开控制中心,会看到以下自启动插件图标,即为安装成功:

    控制中心插件图标

    此时,如果进入/usr/lib/x86_64-linux-gnu/dde-control-center/modules/文件夹,看到编译出的.so文件已经被下载到该文件夹中:

    插件安装位置文件夹

    4、卸载插件

    sudo sh uninstall.sh
    

    重启控制中心,可以看到原本的“自启动管理”图标消失,即为卸载成功。

    分工

    朱元依:插件前端开发

    沈扬:环境配置,编写配置文件

    朱俊杰:后端接口开发(添加、删除、启用、禁用)

    开发计划

    第一步(6/26~7/8)

    • 调研DDE Control Center框架等相关内容
    • 设计前端界面
    • 分工

    第二步(7/9~7/15)

    • 编译教程中的Hello World控制中心插件
    • 设计插件架构

    第三步(7/16~7/22)

    • 配置环境,编译V20示例插件
    • 设计后端接口
    • 修改windowoperationcategory下的文件

    第四步(7/23~7/29)

    • 配置环境,编译Default-AppSelf Start-up插件
    • Debug

    第五步(7/30~8/10)

    • 修改翻译、文字颜色问题
    • 撰写文档

    Wednesday, August 10, 2022

    什么是REUSE

    REUSE是一个工具,准确的来说是一个帮助我们下载开源许可证书和检查开源声明是否合规的脚本:

    使用REUSE配置项目

    安装

    REUSE依赖于本地的python环境,所以需要首先安装python和pip

    sudo apt install python3
    curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
    sudo python3 get-pip.py
    

    然后安装REUSE

    pip3 install --user reuse
    
    

    在此之后,确保它~/.local/bin在您的$PATH中。在 Windows 上,您的环境所需的路径可能类似于 %USERPROFILE%\AppData\Roaming\Python\Python39\Scripts,具体取决于您安装的 Python 版本。

    使用

    初始化项目

    进入一个项目,使用reuse init命令可以初始化项目,此时在你的命令行菜单上会以交互的方式询问你需要的许可证和版权所有人以及邮箱。注意:许可证的名称需要规范:许可证名称

    如果你在这一步遗漏了也没事,可以使用reuse download <许可证名称>的方式下载所需的许可证,这些许可证会一并放入项目下的License文件夹中

    为代码文件添加版权声明

    使用自动化脚本添加

    含有代码逻辑文件添加版权声明方法为:

    reuse addheader --copyright="UnionTech Software Technology Co., Ltd." --license=GPL-3.0-or-later -r <目录>
    

    这只是一个简单的例子,本质上resuse还有更多的用法:

    --copyright-style可以将默认值更改 SPDX-FileCopyrightText为以下样式之一:

    spdx:           SPDX-FileCopyrightText: <year> <statement>
    spdx-symbol:    SPDX-FileCopyrightText: © <year> <statement>
    string:         Copyright <year> <statement>
    string-c:       Copyright (C) <year> <statement>
    string-symbol:  Copyright © <year> <statement>
    symbol:         © <year> <statement>
    

    reuse不会限制你使用多少个copyright 也不会限制你使用的license的个数。

    reuse 会猜测你使用的语言,并且添加对应的注释格式,如果不能正确添加或者需要更多的需求可以考虑下面的方法。

    手动进行添加

    reuse允许你手动进行添加版权声明,只要你的版权声明符合它的规范即可。你所需要的是:充分的耐心,键盘上未曾损坏的Ctrl C V 三个按键即可完成这一切。我个人建议是用上面提供的自动化脚本添加一个版权声明作为模板,然后自己修改下,就可以展现你作为CV工程师的实力了。

    自动进行添加

    理论上reuse不限制你添加版权声明的方式,你可以选择你喜欢的脚本语言来完成这一切。

    为非代码文件添加版权声明

    在项目中是存在非代码文件,比如图片,音频等等。部分也是受到版权保护的,但是我们也没法直接在上面操作(别和我说水印和音频水印,这俩玩意不能无损添加)所以我们要使用一个外置的dep5文件去声明这些非代码文件的版权,dep5文件在执行项目init之后就已经自动帮你生成好了,所以直接写就是的了。比如:

    # css
    Files: *.css
    Copyright: None
    License: CC0-1.0
    

    第一行就是注释 ,第二行是适合的文件,第三行是版权信息(如果使用cc0,则填写None) ,第四行是使用的licence。第一行的注释你可以选择不写,但是为了以后的可读性,强烈建议你写上,甚至可以作为分类的标签。

    第二行文件,支持使用通配符进行模糊匹配,但是不支持正则表达式:仅支持,*匹配任意多个字符 匹配一个字符。所以可以利用通配符来制定匹配某一个文件夹下面的所有文件

    例子:

    # assets
    Files: styleplugins/dstyleplugin/assets/*
    Copyright: UnionTech Software Technology Co., Ltd.
    License: GPL-3.0-or-later
    

    这个例子展示了有版权信息的情况

    例子:

    # png svg
    Files: platformthemeplugin/icons/* styleplugins/chameleon/menu_shadow.svg styles/images/woodbackground.png
           styles/images/woodbutton.png tests/iconengines/builtinengine/icons/actions/icon_Layout_16px.svg
           tests/iconengines/svgiconengine/icon_window_16px.svg
    Copyright: None
    License: CC0-1.0
    

    这个例子展示了为多个文件路径及文件配置版权信息

    强烈推荐你使用路径和具体文件名的方式来制定版权信息,而要避免大范围使用通配符的情况

    例子:

    Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
    Upstream-Name: qt5integration
    Upstream-Contact: UnionTech Software Technology Co., Ltd.  <>
    Source: https://github.com/linuxdeepin/qt5integration
    
    # README
    Files: README.md CHANGELOG.md
    Copyright: None
    License: CC0-1.0
    
    # assets
    Files: styleplugins/dstyleplugin/assets/*
    Copyright: UnionTech Software Technology Co., Ltd.
    License: GPL-3.0-or-later
    
    # Project file
    Files: *.pro *.prf *.pri
    Copyright: None
    License: CC0-1.0
    
    # css
    Files: *.css
    Copyright: None
    License: CC0-1.0
    
    # qrc
    Files: *.qrc
    Copyright: None
    License: CC0-1.0
    
    # png svg
    Files: platformthemeplugin/icons/* styleplugins/chameleon/menu_shadow.svg styles/images/woodbackground.png
           styles/images/woodbutton.png tests/iconengines/builtinengine/icons/actions/icon_Layout_16px.svg
           tests/iconengines/svgiconengine/icon_window_16px.svg
    Copyright: None
    License: CC0-1.0
    
    # sh
    Files: tests/test-recoverage-qmake.sh
    Copyright: None
    License: CC0-1.0
    
    # ignore git
    Files: .git*
    Copyright: None
    License: CC0-1.0
    
    # xml toml json conf yaml
    Files: *.xml *.toml *.json *conf *.yaml
    Copyright: None
    License: CC0-1.0
    
    # rpm
    Files: rpm/*
    Copyright: None
    License: CC0-1.0
    
    # debian
    Files: debian/*
    Copyright: None
    License: CC0-1.0
    
    # Arch
    Files: archlinux/*
    Copyright: None
    License: CC0-1.0
    

    无需版权声明文件的处理方案

    有部分文件是无需版权声明的,比如说某些资源文件脚本文件或者序列化的文件,这种文件有两种处理方式:

    • .gitignore 是用来忽略某些与项目无关的文件和编译过程中产生的文件,同样的如果reuse也会忽略在.gitignore中标记的文件
    • dep5 在执行项目init之后,会在项目目录下产生.reuse/dep5 文件,打开后能看到官方给的范例,如果一个文件不需要特殊版权声明则使用CC0-1.0(会放弃对此文件的所有版权)

    检查项目合规

    reuse lint
    

    这个命令会列出项目文件数,如果项目已经合规,将会以:-)提示,如果项目有些文件不符合开源许可证规范将会以:-(提示,此时你就需要按照上面的提示进行相应的修改即可。

    一般情况下,每个文件都需要有与之相关的版权和许可信息。REUSE规范详细说明了几种方法。总的来说,有这些方法:

    • 将标签放在文件的标题中。
    • 将标签放置在与.license文件相邻的文件中。
    • 将信息放入 DEP5 文件中。

    如果发现一个文件没有与之关联的版权和/或许可信息,则该项目不合规。

    个人建议

    个人建议如果对现有项目使用reuse配置且需要保留版权的时间信息,不要使用一键配置版权头的方式,而是进行手动替换,这里推荐使用vscode的文件筛选功能,筛选出你需要添加的文件,然后cv下去。在配置dep5文件的时候,一定小心通配符的范围,不宜过大。并且对于第三方版权文件一定得小心规避,同样需要完整保留第三方版权信息。

    统信软件常见Dep5书写参考

    参考详细见:

    Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
    Upstream-Name: dtkgui
    Upstream-Contact: UnionTech Software Technology Co., Ltd. <>
    Source: https://github.com/linuxdeepin/dtkgui
    
    # ci
    Files: .github/* .gitlab-ci.yml
    Copyright: None
    License: CC0-1.0
    
    # gitignore #
    Files: .gitignore
    Copyright: None
    License: CC0-1.0
    
    # json conf yaml
    Files: *.json *conf *.yaml
    Copyright: None
    License: CC0-1.0
    
    #interface
    Files: src/util/D* src/kernel/D* src/filedrag/D*
    Copyright: None
    License: CC0-1.0
    
    # rpm
    Files: rpm/*
    Copyright: None
    License: CC0-1.0
    
    # debian
    Files: debian/*
    Copyright: None
    License: LGPL-3.0-or-later
    
    # Arch
    Files: archlinux/*
    Copyright: None
    License: CC0-1.0
    
    # README&doc
    Files: README.md doc/src/*.qdoc
    Copyright: None
    License:  CC-BY-4.0
    
    # DBus
    Files: src/dbus/*.xml
    Copyright: None
    License: CC0-1.0
    
    # Project file
    Files: *.pro *.prf *.pri *.qrc *CMakeLists.txt
    Copyright: None
    License: CC0-1.0
    
    # svg
    Files: src/util/icons/actions/*  src/util/icons/icons/* src/util/icons/texts/*
      tests/images/logo_icon.svg
    Copyright: UnionTech Software Technology Co., Ltd.
    License: LGPL-3.0-or-later
    

    常见开源协议选择

    一般情况下对于文档信息使用CC-BY-4.0 对于我们的一般开源项目使用LGPL-3.0-or-later许可证,如果遇到第三方文件,则是需要保留原有许可证,保留原版版权声明。

    Saturday, July 9, 2022

    这里整理一些关于c++中的匿名函数的知识.

    《C++ primer》上有的内容就不在重述了.这里重点讲一些primer上可能没有的东西.

    捕获的时机

    primer 向我们介绍到: 有两种捕获变量的方式, 值捕获和引用捕获. 其中:

    与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝

    如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存 在的.lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了.

    我们看一段代码:

    auto funtionTimesMod1(int mod) {
      int variableA = mod;
      auto f = [variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test1() {
      cout << "---test1---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod1(c);
      cout << times(a, b) << endl;
    }
    

    这里funtionTimesMod1(int mod)返回一个签名为int(int, int)的函数,这个函数计算 两个参数相乘对mod取模的结果.

    当然可以正常运行,结果为4,如果我们将它换成引用捕获:

    auto funtionTimesMod2(int mod) {
      int variableA = mod;
      auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test2() {
      cout << "---test2---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod2(c);
      cout << times(a, b) << endl;
    }
    

    这段代码就已经不能正常工作了,它的输出是 0.

    我们来看看为啥:

    Process 15820 stopped
    * thread #1, name = 'tmp', stop reason = step in
        frame #0: 0x000000000040096b tmp`funtionTimesMod2(mod=7) at tmp.cpp:17
       14   }
       15
       16   auto funtionTimesMod2(int mod) {
    -> 17     int variableA = mod;
       18     auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
       19     return f;
       20   }
    (lldb) p &variableA
    (int *) $1 = 0x00007fffffffe120
    
    Process 15820 stopped
    * thread #1, name = 'tmp', stop reason = step over
        frame #0: 0x00000000004009dc tmp`test2() at tmp.cpp:25
       22     cout << "---test2---" << endl;
       23     int a = 10, b = 20, c = 7;
       24     auto times = funtionTimesMod2(c);
    -> 25     cout << times(a, b) << endl;
       26   }
       27   auto funtionTimesMod3(int mod) {
       28     int variableA = mod;
    (lldb) p times
    ((anonymous class)) $2 = {
      variableA = 0x00007fffffffe120
    }
    

    可以看到times这个对象中保存下来的variableA只是一个指针,它指向我们之前创建的 局部变量variableA,这个地址在栈上,这意味着当我们真的调用times的时候,局部变 量variableA所在的那片内存已经被使用过了.所以会返回错误的结果.接下来,我们可以 看到实际上调用的时候variableA里面是20,这是因为刚好参数b被放置在 了variableA之前的位置上.

    Process 21271 stopped
    * thread #1, name = 'tmp', stop reason = step in
        frame #0: 0x0000000000400a32 tmp`funtionTimesMod2(this=0x00007fffffffe158, a=10, b=20)::$_1::operator()(int, int) const at tmp.cpp:18
       15
       16   auto funtionTimesMod2(int mod) {
       17     int variableA = mod;
    -> 18     auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
       19     return f;
       20   }
       21   void test2() {
    (lldb) p variableA
    (int) $3 = 20
    (lldb) p &b
    (int *) $4 = 0x00007fffffffe120
    

    lambda 的实现

    根据《C++ Primer》在10.3以及14.8.1的介绍,我们知道一个lambda表达式是由编译器负责 翻译成一个没有类名,重载过调用运算符的对象的.

    这解释了为什么我们只能用auto来定义一个lambda类型的变量,因为这个类是没有名字的, 或者说它的名字是编译器自己生成的,我们并不能知道它叫什么.

    这听起来很美好,我们也确实可以写出代码,用一个自己创建的,重载过调用运算符的对象,来 模拟一个lambda:

    class INT {
    public:
      int num;
      INT(const INT &i) : num(i.num) { cout << "Copy constructor" << endl; }
      INT() = default;
    };
    
    auto funtionTimesMod3(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      auto f = [variableA](int a, int b) {
        cout << "----C" << endl;
        return a % variableA.num * b % variableA.num;
      };
      cout << "----D" << endl;
      return f;
    }
    
    void test3() {
      cout << "---test3---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod3(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    auto funtionTimesMod4(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      } f(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test4() {
      cout << "---test4---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod4(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    

    为了观察发生的值拷贝的时机,以确定我们自己写的这个类的行为和lambda真的完全一致,我 们可以自定义一个class,并且让他发生拷贝构造的时候打印一些信息.

    运行结果如下:

    ---test3---
    ----E
    ----A
    ----B
    Copy constructor
    ----D
    ----F
    ----C
    4
    ----G
    ---test4---
    ----E
    ----A
    ----B
    Copy constructor
    ----D
    ----F
    ----C
    4
    ----G
    

    这又一次说明了说明捕获引起的值拷贝发生在lambda被初始化的时候.

    看起来这两个东西的行为几乎一致, 但是真的是这样吗?

    编译器的优化

    编译器会对我们写的代码做出一些优化,以减少复制对象的次数.如果不了解这点可以看看 这篇博客.

    如果我们关闭返回值优化,那么运行的结果是这样的:

    ---test3---
    ----E
    ----A
    ----B
    Copy constructor
    Copy constructor
    ----D
    Copy constructor
    Copy constructor
    ----F
    ----C
    4
    ----G
    ---test4---
    ----E
    ----A
    ----B
    Copy constructor
    ----D
    Copy constructor
    Copy constructor
    ----F
    ----C
    4
    ----G
    

    可以看到我们自己写的类,少了一次拷贝构造.

    我们先来解释一下lambda为什么会发生这么多次拷贝构造.

    auto funtionTimesMod3(int mod) {
      INT variableA; // 5
      cout << "----A" << endl; // 6 
      variableA.num = mod; // 7
      cout << "----B" << endl; // 8
      auto f = [variableA](int a, int b) { 
        cout << "----C" << endl; // 14
        return a % variableA.num * b % variableA.num; //15
      }; // 9
      cout << "----D" << endl; // 10
      return f; // 11
    }
    
    void test3() {
      cout << "---test3---" << endl; // 1
      int a = 10, b = 20, c = 7; // 2
      cout << "----E" << endl; // 3
      auto times = funtionTimesMod3(c); // 4
      cout << "----F" << endl; // 12
      cout << times(a, b) << endl; // 13
      cout << "----G" << endl; //16
    }
    

    首先我们的程序是按照如上的顺序运行的.

    可以看到前两个拷贝构造发生在9,而第3、4次发生在11.

    如果完全按照语义来看的话,9这句话可以有两种理解方式:

    1. 创建一个lambda对象,对象名字叫f,这个对象的内容就是后面那个lambda表达式.
    2. 创建一个lambda表达式,然后将其作为参数,调用同类型对象f的拷贝构造函数.

    如果按照第一种方法来理解,那么第9行发生两次拷贝构造就不是很能理解了.

    所以应该是第二种.

    在10之后也发生了两次拷贝调用.应该是先建立了一个变量用来做返回值,比如说叫r,然后 将f赋值给返回值变量r,然后返回值变量r在被赋值给test3()中的times,这样发生的两次拷 贝构造.

    auto funtionTimesMod4(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      } f(variableA); // <- 这里
      cout << "----D" << endl;
      return f;
    }
    
    void test4() {
      cout << "---test4---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod4(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    

    之前自己模拟lambda的这个代码,我们在注释标注的那个位置的实现和lambda中,“先建一个 右值,然后拷贝构造出f"的行为不太一样.导致这里少了一次拷贝构造.所以实际上应该是这 样:

    auto funtionTimesMod5(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      };
      function f = function(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test5() {
      cout << "---test5---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod5(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    

    实际上没必要纠结那么多,正常编译的话,其实是不会去先建一个右值的对象的.

    代码

    #include <bits/stdc++.h>
    using namespace std;
    
    auto funtionTimesMod1(int mod) {
      int variableA = mod;
      auto f = [variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test1() {
      cout << "---test1---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod1(c);
      cout << times(a, b) << endl;
    }
    
    auto funtionTimesMod2(int mod) {
      int variableA = mod;
      auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
      return f;
    }
    void test2() {
      cout << "---test2---" << endl;
      int a = 10, b = 20, c = 7;
      auto times = funtionTimesMod2(c);
      cout << times(a, b) << endl;
    }
    
    class INT {
    public:
      int num;
      INT(const INT &i) : num(i.num) { cout << "Copy constructor" << endl; }
      INT() = default;
    };
    
    auto funtionTimesMod3(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      auto f = [variableA](int a, int b) {
        cout << "----C" << endl;
        return a % variableA.num * b % variableA.num;
      };
      cout << "----D" << endl;
      return f;
    }
    
    void test3() {
      cout << "---test3---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod3(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    auto funtionTimesMod4(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      } f(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test4() {
      cout << "---test4---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod4(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    auto funtionTimesMod5(int mod) {
      INT variableA;
      cout << "----A" << endl;
      variableA.num = mod;
      cout << "----B" << endl;
      class function {
        const INT variableA;
    
      public:
        function(const INT &variableA) : variableA(variableA) {}
        int operator()(int a, int b) {
          cout << "----C" << endl;
          return a % variableA.num * b % variableA.num;
        }
      };
      function f = function(variableA);
      cout << "----D" << endl;
      return f;
    }
    
    void test5() {
      cout << "---test5---" << endl;
      int a = 10, b = 20, c = 7;
      cout << "----E" << endl;
      auto times = funtionTimesMod5(c);
      cout << "----F" << endl;
      cout << times(a, b) << endl;
      cout << "----G" << endl;
    }
    
    int main() {
      test1();
      test2();
      test3();
      test4();
      test5();
    }
    

    Saturday, June 18, 2022

    Bcache 是 Linux 下的一个块缓存内核模块,可用于使用单个固态硬盘为一个或多个机械硬盘加速,也可用于使用本地磁盘为网络磁盘的加速。

    Bcache 有比较灵活的缓存模式,支持安全读写缓存(writethrough)、高性能读写缓存(writeback)、只读缓存(writearound)、停用缓存(none)等模式,并且可在线动态调整。

    Bcache 还会自动识别顺序写入,当发现正在进行顺序写入时跳过缓存层,以减少缓存设备(固态硬盘)的损耗,延长固态硬盘的寿命。

    Monday, May 30, 2022

    DDE 自启动管理插件可用于集中管理Deepin系统中软件开机自启动设置的插件。

    项目链接:DDE 自启动管理插件 github仓库

    参赛信息

    本项目参加2023全国大学生计算机系统能力大赛操作系统设计赛-功能挑战赛,选题为proj223-control-center-startup-management-plugin

    参赛小组:

    • 复旦大学 朱元依
    • 复旦大学 沈扬
    • 复旦大学 朱俊杰

    指导老师:

    • 复旦大学 张亮
    • 复旦大学 陈辰
    • 企业导师 王子冲

    完成情况

    项目文档位于根目录的初赛报告.md
    博客地址:https://y-aang.github.io (题目:DDE 自启动管理插件)

    任务完成情况
    (必须)完成一个控制中心插件,能够展示当前所有开机启动项的列表完成
    (必须)插件以单独的仓库提供,并能够单独构建,不需要合并入 dde-control-center 项目完成
    (必须)能够在插件中,通过用户界面的交互来管理(添加、删除、启用、禁用)开机启动项完成
    (必须)编写博客,记录开发过程的心得与体会,并将博客投递至 planet.deepin.org完成博客编写,待deepin官方审核

    成果展示

    功能展示视频链接:

    链接:百度网盘链接

    提取码:hoo8

    插件图标:

    图标

    自启动管理窗口:

    前端界面

    部署方式

    1、系统环境

    开发环境:Deepin 20Beta版

    系统架构:x86

    镜像下载链接:Deepin 操作系统下载链接

    虚拟机平台:WMware Workstation 16Pro

    操作系统环境搭建参考博客:环境搭建博客

    2、安装依赖库

    安装包 build-essentialgitg++cmakeddedtk

    sudo apt install build-essential git g++ cmake
    sudo apt install dde-dock-dev libdtkwidget-dev
    

    3、安装插件

    克隆该仓库,进入文件夹后以sudo权限运行install.sh脚本

    git clone https://github.com/PinappleUnderTheSea/os223.git
    cd os233
    sudo su
    sh install.sh
    

    出现成果展示章节的图标,即为安装成功。

    提交仓库目录和文件描述

    .
    ├── CMakeLists.txt
    ├── README.md
    ├── aboutdialog.cpp    #关于窗口的实现文件
    ├── aboutdialog.h     #关于窗口的头文件
    ├── aboutdialog.ui     #关于窗口的UI文件
    ├── appletwidget.cpp   #自启动管理窗口的实现文件
    ├── appletwidget.h    #自启动管理窗口的头文件
    ├── images        #图片
    │ ├── QT_IDE.png
    │ ├── QT_前端.png
    │ ├── deepin自启动修改.png
    │ ├── 类图.jpg
    │ ├── 右键.png
    │ ├── 图标.png
    │ ├── 结果.png
    │ ├── 中期类图.jpg
    │ ├── 中期测试.png
    │ └── 前端界面.png
    ├── install.sh        #插件安装脚本
    ├── main_aboutdialog_test.cpp #关于窗口的测试文件
    ├── main_test.cpp      #测试文件
    ├── mainwidget.cpp     #插件类的实现文件
    ├── mainwidget.h      #插件类的头文件
    ├── self_startup.json     #插件的元数据文件,指明了当前插件所使用的 dde-dock 的接口版本
    ├── self_startup.pro     #辅助 cmake 的配置文件
    ├── self_startup.qrc     #用于展示插件图片
    ├── selfstartupplugin.cpp   #部件类的实现文件
    ├── selfstartupplugin.h    #部件类的头文件
    ├── uninstall.sh        #插件卸载脚本
    ├── 初赛报告.md
    └── 过程文档.md

    分工

    朱元依:插件类框架开发、部件类前端开发

    沈扬:自启动管理功能逻辑设计、插件类右键功能开发

    朱俊杰:自启动管理窗口后端接口开发(添加、删除、启用、禁用)

    开发计划

    第一步(4/9~4/18)

    • 调研Deepindde-dockQT框架等相关内容
    • 设计项目方案
    • 分工

    第二步(4/19~5/2)

    • 搭建主体插件类的框架
    • 设计启动项管理窗口的前端展示页面

    第三步(5/3~5/13)

    • 开发部件类接口
    • 完善插件类功能

    第四步(5/14~5/21)

    • 插件类右键功能开发
    • 完成配置文件

    第五步(5/22~5/31)

    • Debug
    • 撰写文档

    Thursday, March 10, 2022

    在项目开发中,我们通常会使用条件编译对代码进行裁剪,选择性地排除不需要的代码,比如在某个平台下完全不支持某个功能,那么这个功能就不应该被编译。

    一般我们使用宏来判断代码,选择性的挑选需要编译的部分,并在构建系统中开启这样的条件。

    #ifdef XXXXXXXXXX
        std::cout << "hello world!" << std::endl;
    #else
        std::cout << "good bye!" << std::endl;
    #endif
    

    在 C 语言的项目中,这样的行为是很正常的,甚至在 C++ 项目中,我们也会选择使用宏进行条件判断。

    但是如果定义的宏多了,则很容易导致代码碎片化,并且无法直观地看到工作流程。

    例如这样的代码:

    #define KWIN_VERSION_CHECK(major, minor, patch, build) ((major<<24)|(minor<<16)|(patch<<8)|build)
    #ifdef KWIN_VERSION_STR
    #define KWIN_VERSION KWIN_VERSION_CHECK(KWIN_VERSION_MAJ, KWIN_VERSION_MIN, KWIN_VERSION_PAT, KWIN_VERSION_BUI)
    #endif
    
    #if defined(KWIN_VERSION) && KWIN_VERSION < KWIN_VERSION_CHECK(5, 21, 3, 0)
    typedef int TimeArgType;
    #else
    typedef std::chrono::milliseconds TimeArgType;
    #endif
    
    #if defined(Q_OS_LINUX) && !defined(QT_NO_DYNAMIC_LIBRARY) && !defined(QT_NO_LIBRARY)
    QT_BEGIN_NAMESPACE
    QFunctionPointer qt_linux_find_symbol_sys(const char *symbol);
    QT_END_NAMESPACE
    QFunctionPointer KWinUtils::resolve(const char *symbol)
    {
        return QT_PREPEND_NAMESPACE(qt_linux_find_symbol_sys)(symbol);
    #else
    QFunctionPointer KWinUtils::resolve(const char *symbol)
    {
        static QString lib_name = "kwin.so." + qApp->applicationVersion();
    
        return QLibrary::resolve(lib_name, symbol);
    #endif
    }
    

    从上面的例子中可以看到,代码中一旦出现大量重复的判断条件,代码非常不直观,而且被宏分割成了很多部分。

    在一次偶然的机会,我看到了一篇介绍 C++ 17 中的 if constexpr 的用法,可以在编译期进行一些计算,虽然我很早就知道了 constexpr 的用法,但是大家举的例子基本上都是数值计算,让编译器在编译期间将数值进行计算,从而减轻运行时的消耗,我也从来想到其他用法,所以一直没有在项目中使用到。

    constexpr 的作用并不是编译期计算数值,而是编译期进行的代码分析,如果代码较小且非常直观,比如大家经常举的例子,在编译期间计算斐波那契数列,这种例子即使不使用 constexpr 显式要求,编译器也会帮助我们开启优化,直接给出结果。

    但是如果代码非常复杂,编译器就不一定会为我们做这样的优化,就需要我们手动标记可以计算的位置,要求编译器在编译期间进行求值和优化。

    我设想的是,使用 cmake 在构建时,先生成一份文件,将开关的值记录下来,在需要进行判断的地方,就可以直接使用 if constexpr 进行条件判断,在编译期间,编译器会发现有一个分支确定不会被执行(相当于 if(false) {}),那么这个分支就不会进行编译,直接剔除。

    CMakeLists.txt 中需要做一些工作,将编译参数加入构建系统。

    option (ENABLE_MODULE "Enable Module" ON)
    
    if(ENABLE_MODULE)
        set(ENABLE_MODULE "1")
    else()
        set(ENABLE_MODULE "0")
    endif(ENABLE_MODULE)
    
    configure_file (
      "${CMAKE_CURRENT_SOURCE_DIR}/options/options.h.in"
      "${CMAKE_CURRENT_BINARY_DIR}/options/options.h"
    )
    

    在 options/options.h.in 文件里,按照 cmake 的要求将变量导入进文件中,进行内容替换。

    #pragma once
    
    #cmakedefine01 ENABLE_MODULE
    

    这里我仍然使用的是宏定义,也可以直接写成如下形式:

    option (ENABLE_MODULE "Enable Module" ON)
    
    if(ENABLE_MODULE)
        set(ENABLE_MODULE "true")
    else()
        set(ENABLE_MODULE "false")
    endif(ENABLE_MODULE)
    
    configure_file (
      "${CMAKE_CURRENT_SOURCE_DIR}/options/options.h.in"
      "${CMAKE_CURRENT_BINARY_DIR}/options/options.h"
    )
    
    #pragma once
    
    const bool ENABLE_MODULE{@ENABLE_MODULE@};
    

    在 main.cpp 中写一段测试代码:

    #include "options/options.h"
    
    #include <iostream>
    
    int main() {
        if constexpr (ENABLE_MODULE) {
            std::cout << "Now Enable Module" << std::endl;
        }
    
        if constexpr (!ENABLE_MODULE) {
            std::cout << "Now Disable Module" << std::endl;
        }
    
        return 0;
    }
    

    执行结果是符合预期的。

    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:39]
    $ cmake ../ -G Ninja -DENABLE_MODULE=ON
    -- The CXX compiler identification is AppleClang 13.0.0.13000029
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /Users/lxz/Develop/constexpr-demo/build
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:45]
    $ ninja
    [2/2] Linking CXX executable src/constexpr
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:48]
    $ ./src/constexpr
    Now Enable Module
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:52]
    $ cmake ../ -G Ninja -DENABLE_MODULE=OFF
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /Users/lxz/Develop/constexpr-demo/build
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:28:58]
    $ ninja
    [2/2] Linking CXX executable src/constexpr
    
    # lxz @ lxz-MacBook-Pro in ~/Develop/constexpr-demo/build on git:master o [13:29:00]
    $ ./src/constexpr
    Now Disable Module
    

    虽然结果是符合的,但是我们其实不确定是否真的在编译期间就完成了代码剔除,所以使用命令进行汇编,查看汇编中是否包含了判断指令和两段输出的字符串。

    clang -S main.cpp -o main.s -I./
    

    main.s

    	.text
    	.file	"main.cpp"
    	.globl	main                            // -- Begin function main
    	.p2align	2
    	.type	main,@function
    main:                                   // @main
    	.cfi_startproc
    // %bb.0:
    	stp	x29, x30, [sp, #-32]!           // 16-byte Folded Spill
    	str	x19, [sp, #16]                  // 8-byte Folded Spill
    	mov	x29, sp
    	.cfi_def_cfa w29, 32
    	.cfi_offset w19, -16
    	.cfi_offset w30, -24
    	.cfi_offset w29, -32
    	adrp	x19, :got:_ZSt4cout
    	ldr	x19, [x19, :got_lo12:_ZSt4cout]
    	adrp	x1, .L.str
    	add	x1, x1, :lo12:.L.str
    	mov	w2, #18
    	mov	x0, x19
    	bl	_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
    	ldr	x8, [x19]
    	ldur	x8, [x8, #-24]
    	add	x8, x8, x19
    	ldr	x19, [x8, #240]
    	cbz	x19, .LBB0_5
    // %bb.1:
    	ldrb	w8, [x19, #56]
    	cbz	w8, .LBB0_3
    // %bb.2:
    	ldrb	w1, [x19, #67]
    	b	.LBB0_4
    .LBB0_3:
    	mov	x0, x19
    	bl	_ZNKSt5ctypeIcE13_M_widen_initEv
    	ldr	x8, [x19]
    	mov	w1, #10
    	mov	x0, x19
    	ldr	x8, [x8, #48]
    	blr	x8
    	mov	w1, w0
    .LBB0_4:
    	adrp	x0, :got:_ZSt4cout
    	ldr	x0, [x0, :got_lo12:_ZSt4cout]
    	bl	_ZNSo3putEc
    	bl	_ZNSo5flushEv
    	ldr	x19, [sp, #16]                  // 8-byte Folded Reload
    	mov	w0, wzr
    	ldp	x29, x30, [sp], #32             // 16-byte Folded Reload
    	ret
    .LBB0_5:
    	bl	_ZSt16__throw_bad_castv
    .Lfunc_end0:
    	.size	main, .Lfunc_end0-main
    	.cfi_endproc
                                            // -- End function
    	.section	.text.startup,"ax",@progbits
    	.p2align	2                               // -- Begin function _GLOBAL__sub_I_main.cpp
    	.type	_GLOBAL__sub_I_main.cpp,@function
    _GLOBAL__sub_I_main.cpp:                // @_GLOBAL__sub_I_main.cpp
    	.cfi_startproc
    // %bb.0:
    	stp	x29, x30, [sp, #-32]!           // 16-byte Folded Spill
    	str	x19, [sp, #16]                  // 8-byte Folded Spill
    	mov	x29, sp
    	.cfi_def_cfa w29, 32
    	.cfi_offset w19, -16
    	.cfi_offset w30, -24
    	.cfi_offset w29, -32
    	adrp	x19, _ZStL8__ioinit
    	add	x19, x19, :lo12:_ZStL8__ioinit
    	mov	x0, x19
    	bl	_ZNSt8ios_base4InitC1Ev
    	adrp	x0, :got:_ZNSt8ios_base4InitD1Ev
    	ldr	x0, [x0, :got_lo12:_ZNSt8ios_base4InitD1Ev]
    	mov	x1, x19
    	ldr	x19, [sp, #16]                  // 8-byte Folded Reload
    	adrp	x2, __dso_handle
    	add	x2, x2, :lo12:__dso_handle
    	ldp	x29, x30, [sp], #32             // 16-byte Folded Reload
    	b	__cxa_atexit
    .Lfunc_end1:
    	.size	_GLOBAL__sub_I_main.cpp, .Lfunc_end1-_GLOBAL__sub_I_main.cpp
    	.cfi_endproc
                                            // -- End function
    	.type	_ZStL8__ioinit,@object          // @_ZStL8__ioinit
    	.local	_ZStL8__ioinit
    	.comm	_ZStL8__ioinit,1,1
    	.hidden	__dso_handle
    	.type	.L.str,@object                  // @.str
    	.section	.rodata.str1.1,"aMS",@progbits,1
    .L.str:
    	.asciz	"Now Disable Module" // 关键在这里
    	.size	.L.str, 19
    
    	.section	.init_array,"aw",@init_array
    	.p2align	3
    	.xword	_GLOBAL__sub_I_main.cpp
    	.ident	"clang version 13.0.1"
    	.section	".note.GNU-stack","",@progbits
    	.addrsig
    	.addrsig_sym _GLOBAL__sub_I_main.cpp
    	.addrsig_sym _ZStL8__ioinit
    	.addrsig_sym __dso_handle
    	.addrsig_sym _ZSt4cout
    

    查看整个 main.s 汇编,发现只在 .L.str 段中有预期的文本字符串,可以得出结论,代码是在编译期间完成了剔除,符合我们的要求。

    原文链接:https://blog.justforlxz.com/2022/03/10/if-constexpr/

    Friday, March 4, 2022

    作为一名使用 Qt 的开发人员,Qt 为我提供了大量好用的基础设施,例如广受好评的 QString、QNetwork之类的,这是 Qt 平台为我提供的帮助,我只需要在这个平台上开发就足够了。

    同样作为一名 C++ 开发人员,C++ 标准库也是我需要用的基础设施,但是标准库提供的功能就不如 Qt 了,最令人诟病的就是 C++ 的 std::string,业内充斥着对 std::string 的不屑与谩骂。

    但是这一情况将会在 C++ 20 标准后改善,这里不具体展开,将来我会准备写一份 C++ 20 的功能介绍。

    今天在开发项目的时候,为了节省资源,我仅使用标准库完成开发,缺少了 Qt 平台为我提供的有利帮助,项目开发的难度瞬间增加,在此期间,我发现了 C++ 的目录操作似乎能比的上 Qt 提供的 QDir封装,我打算在本篇文章中介绍一下这两者的不同。

    QDir

    Qt 的基本思路是继承大于组合,所以 Qt 为我们提供的都是各种继承的类,在 Qt 中,我们使用 QDir 类进行目录操作。

    QDir 类使用相对或绝对文件路径来指向一个文件或目录。如果总是使用 “/” 作为目录分隔符,Qt 将会把你的路径转化为符合底层的操作系统的。

    QDir 类由于不涉及 IO 的具体操作,所以没有继承自 QObject 或者其他 QIO 的类。

    QDir 提供了非常多的方法,可以方便的获取目录的名称、绝对路径、相对路径、设置目录过滤器等。

    一个基本的用法如下:

    QDir dir("/tmp");
    if (!dir.exists()) {
        return
    }
    
    for (const QFileInfo& item : dir.entryInfoList()) {
        if (item.isDir()) {
            // ...
          }
    }
    
    dir.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks);
    dir.setSorting(QDir::Size | QDir::Reversed);
    
    const QFileInfoList& list = dir.entryInfoList();
    for (const QFileInfo& item : list) {
        // ...
    }
    

    在上面的示例代码中可以看到,QDir 整体是非常符合面向对象的,我们使用 QDir 对象,对目录执行各种操作,涉及到具体的文件(目录是特殊的文件),我们可以使用 QFileInfo 类获取具体文件的信息。

    除了以上演示涉及到的方法,QDir 还有很多其他方法,使用 count() 方法统计目录内文件和目录的数量,使用 remove() 方法删除指定的目录,这里就不一一列举了。

    QDir 支持设置多种过滤,过滤(Filter) 和 排序(Sorting) 都会影响到 entryInfoList 方法返回的内容。

    QDir 接受的过滤器枚举:

    枚举枚举值描述
    QDir::Dirs0x001列出与过滤器匹配的目录。
    QDir::AllDirs0x400列出所有目录;即不要将过滤器应用于目录名称。
    QDir::Files0x002列出文件。
    QDir::Drives0x004列出磁盘驱动器(在Unix下忽略)。
    QDir::NoSymLinks0x008不要列出符号链接(不支持符号链接的操作系统忽略)。
    QDir::NoDotAndDotDotNoDotNoDotDot
    QDir::NoDot0x2000不要列出特殊条目“.”。
    QDir::NoDotDot0x4000不要列出特殊条目“..”。
    QDir::AllEntriesDirsFiles
    QDir::Readable0x010列出应用程序具有读取访问权限的文件。可读值需要与Dirs或Files合并。
    QDir::Writable0x020列出应用程序具有写入访问权限的文件。可写值需要与Dirs或Files合并。
    QDir::Executable0x040列出应用程序具有执行访问权限的文件。可执行值需要与Dirs或Files合并。
    QDir::Modified0x080仅列出已修改的文件(在Unix上忽略)。
    QDir::Hidden0x100列出隐藏的文件(在Unix上,以“.”开头的文件)。
    QDir::System0x200列出系统文件(包括Unix、FIFO、套接字和设备文件;在Windows上,包括.lnk文件)
    QDir::CaseSensitive0x800过滤器应该区分大小写。

    QDir 接受的排序枚举:

    枚举枚举值描述
    QDir::Name0x00按名称排序。
    QDir::Time0x01按时间(修改时间)排序。
    QDir::Size0x02按文件大小排序。
    QDir::Type0x80按文件类型(扩展名)排序。
    QDir::Unsorted0x03不要排序。
    QDir::NoSort-1默认情况下未排序。
    QDir::DirsFirst0x04先放目录,然后放文件。
    QDir::DirsLast0x20先放文件,然后放目录。
    QDir::Reversed0x08颠倒排序顺序。
    QDir::IgnoreCase0x10不区分大小写的排序。
    QDir::LocaleAware0x40使用当前区域设置对项目进行适当排序。

    std::filesystem

    上面介绍了 Qt 的设计风格,而标准库的设计风格是组合大于继承,标准库提供各种非常具体的类或者函数,将一个系列的操作拆分为各个子项,最终完成任务。

    这里使用一个小例子来说明一下:

    std::filesystem::path tmp{"/tmp"};
    if (!std::filesystem::exists(tmp)) {
        std::cout << "目录不存在" << std::endl;
        return;
    }
    
    const bool result = std::filesystem::create_directories(tmp);
    if (!result) {
        std::cout << "目录创建失败,也许是已经存在。" << std::endl;
    }
    
    for (auto const& dir_entry : std::filesystem::directory_iterator(tmp)) {
        if (dir_entry.is_directory()) {
            // ...
        }
    }
    
    if (std::filesystem::is_directory(tmp)) {
        // ...
    }
    

    可以看到,C++ 标准库使用多个类共同完成了对目录的检查和遍历,这种基于组合的方式可以带来更多的灵活性,如果需要对某个部分进行修改,只需要继承特定类就可以完成,如果是 Qt 的 QDir,则不是很轻松。

    在使用标准库的时候需要注意的是,标准库通常不会约束使用者,使用当前的例子举例,QDir 提供了过滤器枚举,可以帮助开发者简单的实现文件过滤功能,但是 std::filesystem 则不提供这种接口,标准库提供了机制,但是不提供策略,开发者需要使用标准库提供的各种接口,组合 出自己的业务,所以如果想要使用标准库的时候也能实现过滤和排序,就只能自己提供相应的操作。

    std::filesystem::path tmp{ "/tmp" };
    std::vector<std::filesystem::directory_entry> list;
    std::vector<std::string> filter{ ".jpg", ".txt" };
    std::filesystem::directory_iterator iter{ tmp };
    std::copy_if(iter.begin(),
                 iter.end(),
                 std::back_inserter(list),
                 [filter](std::filesystem::directory_entry entry) {
                    return !entry.is_directory() && 
                           std::find_if(filter.begin(),
                                        filter.end(),
                                        [entry](std::string s) {
                                          return entry.path().extension() == s;
                                        }
                           );
                  }
    );
    

    可以看到,代码变得异常丑陋(逃

    标准库提供了一些比较方便的函数,例如 std::copy_if、 std::back_inserter 和 std::find_if 等,还有一个较为常用的 std::transform。标准库提供了迭代器抽象,这样我们可以使用迭代器对象和迭代器算法,方便的进行各种遍历、复制和转换。

    从上面的例子可以看出,Qt 确实为开发者提供了很好的帮助,这是 Qt 作为一个平台力所能及的工作,当然,即使我们使用 Qt,也还是可以写出上面一样的代码。

    总结

    以上就是简单的 QDir 和 std::filesystem 的不同,综合来看,Qt 库和标准库其实各有优缺,他们的目标和面向的开发者是不同的,Qt 最近一直在尝试使用标准库的内容来代替自己的一部分组件,最新的Qt6 就已经升级到了 C++ 17 标准,将 qSort 宏改为了使用 std::sort,也许在不久的将来,我们再也不用为使用 Qt 还是标准库而争论或者站队。

    引用资料 std::filesystem https://en.cppreference.com/w/cpp/filesystem QDir https://doc.qt.io/qt-5/qdir.html

    原文链接:https://blog.justforlxz.com/2022/03/04/qdir-stdfilesystem/

    作为一名使用 Qt 的开发人员,Qt 为我提供了大量好用的基础设施,例如广受好评的 QString、QNetwork之类的,这是 Qt 平台为我提供的帮助,我只需要在这个平台上开发就足够了。

    同样作为一名 C++ 开发人员,C++ 标准库也是我需要用的基础设施,但是标准库提供的功能就不如 Qt 了,最令人诟病的就是 C++ 的 std::string,业内充斥着对 std::string 的不屑与谩骂。

    但是这一情况将会在 C++ 20 标准后改善,这里不具体展开,将来我会准备写一份 C++ 20 的功能介绍。

    今天在开发项目的时候,为了节省资源,我仅使用标准库完成开发,缺少了 Qt 平台为我提供的有利帮助,项目开发的难度瞬间增加,在此期间,我发现了 C++ 的目录操作似乎能比的上 Qt 提供的 QDir封装,我打算在本篇文章中介绍一下这两者的不同。

    QDir

    Qt 的基本思路是继承大于组合,所以 Qt 为我们提供的都是各种继承的类,在 Qt 中,我们使用 QDir 类进行目录操作。

    QDir 类使用相对或绝对文件路径来指向一个文件或目录。如果总是使用 “/” 作为目录分隔符,Qt 将会把你的路径转化为符合底层的操作系统的。

    QDir 类由于不涉及 IO 的具体操作,所以没有继承自 QObject 或者其他 QIO 的类。

    QDir 提供了非常多的方法,可以方便的获取目录的名称、绝对路径、相对路径、设置目录过滤器等。

    一个基本的用法如下:

    QDir dir("/tmp");
    if (!dir.exists()) {
    return
    }

    for (const QFileInfo& item : dir.entryInfoList()) {
    if (item.isDir()) {
    // ...
    }
    }

    dir.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks);
    dir.setSorting(QDir::Size | QDir::Reversed);

    const QFileInfoList& list = dir.entryInfoList();
    for (const QFileInfo& item : list) {
    // ...
    }

    在上面的示例代码中可以看到,QDir 整体是非常符合面向对象的,我们使用 QDir 对象,对目录执行各种操作,涉及到具体的文件(目录是特殊的文件),我们可以使用 QFileInfo 类获取具体文件的信息。

    除了以上演示涉及到的方法,QDir 还有很多其他方法,使用 count() 方法统计目录内文件和目录的数量,使用 remove() 方法删除指定的目录,这里就不一一列举了。

    QDir 支持设置多种过滤,过滤(Filter) 和 排序(Sorting) 都会影响到 entryInfoList 方法返回的内容。

    QDir 接受的过滤器枚举:

    枚举枚举值描述
    QDir::Dirs0x001列出与过滤器匹配的目录。
    QDir::AllDirs0x400列出所有目录;即不要将过滤器应用于目录名称。
    QDir::Files0x002列出文件。
    QDir::Drives0x004列出磁盘驱动器(在Unix下忽略)。
    QDir::NoSymLinks0x008不要列出符号链接(不支持符号链接的操作系统忽略)。
    QDir::NoDotAndDotDotNoDotNoDotDot
    QDir::NoDot0x2000不要列出特殊条目“.”。
    QDir::NoDotDot0x4000不要列出特殊条目“..”。
    QDir::AllEntriesDirsFiles
    QDir::Readable0x010列出应用程序具有读取访问权限的文件。可读值需要与Dirs或Files合并。
    QDir::Writable0x020列出应用程序具有写入访问权限的文件。可写值需要与Dirs或Files合并。
    QDir::Executable0x040列出应用程序具有执行访问权限的文件。可执行值需要与Dirs或Files合并。
    QDir::Modified0x080仅列出已修改的文件(在Unix上忽略)。
    QDir::Hidden0x100列出隐藏的文件(在Unix上,以“.”开头的文件)。
    QDir::System0x200列出系统文件(包括Unix、FIFO、套接字和设备文件;在Windows上,包括.lnk文件)
    QDir::CaseSensitive0x800过滤器应该区分大小写。

    QDir 接受的排序枚举:

    枚举枚举值描述
    QDir::Name0x00按名称排序。
    QDir::Time0x01按时间(修改时间)排序。
    QDir::Size0x02按文件大小排序。
    QDir::Type0x80按文件类型(扩展名)排序。
    QDir::Unsorted0x03不要排序。
    QDir::NoSort-1默认情况下未排序。
    QDir::DirsFirst0x04先放目录,然后放文件。
    QDir::DirsLast0x20先放文件,然后放目录。
    QDir::Reversed0x08颠倒排序顺序。
    QDir::IgnoreCase0x10不区分大小写的排序。
    QDir::LocaleAware0x40使用当前区域设置对项目进行适当排序。

    std::filesystem

    上面介绍了 Qt 的设计风格,而标准库的设计风格是组合大于继承,标准库提供各种非常具体的类或者函数,将一个系列的操作拆分为各个子项,最终完成任务。

    这里使用一个小例子来说明一下:

    std::filesystem::path tmp{"/tmp"};
    if (!std::filesystem::exists(tmp)) {
    std::cout << "目录不存在" << std::endl;
    return;
    }

    const bool result = std::filesystem::create_directories(tmp);
    if (!result) {
    std::cout << "目录创建失败,也许是已经存在。" << std::endl;
    }

    for (auto const& dir_entry : std::filesystem::directory_iterator(tmp)) {
    if (dir_entry.is_directory()) {
    // ...
    }
    }

    if (std::filesystem::is_directory(tmp)) {
    // ...
    }

    可以看到,C++ 标准库使用多个类共同完成了对目录的检查和遍历,这种基于组合的方式可以带来更多的灵活性,如果需要对某个部分进行修改,只需要继承特定类就可以完成,如果是 Qt 的 QDir,则不是很轻松。

    在使用标准库的时候需要注意的是,标准库通常不会约束使用者,使用当前的例子举例,QDir 提供了过滤器枚举,可以帮助开发者简单的实现文件过滤功能,但是 std::filesystem 则不提供这种接口,标准库提供了机制,但是不提供策略,开发者需要使用标准库提供的各种接口,组合 出自己的业务,所以如果想要使用标准库的时候也能实现过滤和排序,就只能自己提供相应的操作。

    std::filesystem::path tmp{ "/tmp" };
    std::vector<std::filesystem::directory_entry> list;
    std::vector<std::string> filter{ ".jpg", ".txt" };
    std::filesystem::directory_iterator iter{ tmp };
    std::copy_if(iter.begin(),
    iter.end(),
    std::back_inserter(list),
    [filter](std::filesystem::directory_entry entry) {
    return !entry.is_directory() &&
    std::find_if(filter.begin(),
    filter.end(),
    [entry](std::string s) {
    return entry.path().extension() == s;
    }
    );
    }
    );

    可以看到,代码变得异常丑陋(逃

    标准库提供了一些比较方便的函数,例如 std::copy_if、 std::back_inserter 和 std::find_if 等,还有一个较为常用的 std::transform。标准库提供了迭代器抽象,这样我们可以使用迭代器对象和迭代器算法,方便的进行各种遍历、复制和转换。

    从上面的例子可以看出,Qt 确实为开发者提供了很好的帮助,这是 Qt 作为一个平台力所能及的工作,当然,即使我们使用 Qt,也还是可以写出上面一样的代码。

    总结

    以上就是简单的 QDir 和 std::filesystem 的不同,综合来看,Qt 库和标准库其实各有优缺,他们的目标和面向的开发者是不同的,Qt 最近一直在尝试使用标准库的内容来代替自己的一部分组件,最新的Qt6 就已经升级到了 C++ 17 标准,将 qSort 宏改为了使用 std::sort,也许在不久的将来,我们再也不用为使用 Qt 还是标准库而争论或者站队。

    引用资料
    std::filesystem https://en.cppreference.com/w/cpp/filesystem
    QDir https://doc.qt.io/qt-5/qdir.html