C++ Cookbook by Eric
Posted songyuc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Cookbook by Eric相关的知识,希望对你有一定的参考价值。
环境配置:g++-11
g++
跟gcc
之间没有依赖关系,两者分别对应面向C++和C语言的编译程序,关于gcc
和g++
的区别,请参考知乎回答《gcc和g++是什么关系? ——gcc 和 g++ 的区别》
Ubuntu-20.04官方源中目前最高提供了 gcc-11的安装包;
Note
我们尝试在Ubuntu-20.04上执行命令安装 gcc-12:sudo apt install gcc-12 g++-12
不过报错:
E: 无法定位软件包 gcc-12
E: 无法定位软件包 g++-12
所以目前我们准备在 Ubuntu-20.04使用gcc-11。
查询Ubuntu默认使用的gcc版本
cat /proc/version
示例结果:
Linux version 5.4.0-128-generic (buildd@lcy02-amd64-017) (gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)) #144-Ubuntu SMP Tue Sep 20 11:00:04 UTC 2022
这里可以看到当前系统默认使用的gcc版本是9.4.0。
查看已经安装gcc&g++的文件目录
查看gcc版本
ls /usr/bin/gcc*
查看g++版本
ls /usr/bin/g++*
设置gcc&g++版本优先级
Gcc
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 30
G++
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 30
查看运行时gcc&g++版本
Gcc
gcc --version
G++
g++ --version
Note
这里我们在终端查询版本时,没有使用简化命令gcc | g++ -v
,这是因为使用g++ -v
查询g++版本时,输出信息中会包含gcc的字样,看起来有些迷惑,并且输出信息较多显得有些复杂,为了查询结果的直观感受,这里我们统一使用gcc | g++ --version
。
使用C++输出编译器版本号
cout << __VERSION__ << endl;
1. Basics
1.1 文件类型
.h
:C++头文件。
.cc
:CUDA语境下的CPU代码文件。
.cu
:CUDA语境下的GPU代码文件。
1.1.1 头文件
引入头文件
C++在引入头文件时,有下面两种方式:
1. <file_name>
:仅适用于标准库文件的引入,例如
#include <iostream>
2. file_name.h
:适用于自定义的头文件、以及兼容C方式的头文件,例如
#include <iostream.h>
#include <cuda_runtime_api.h>
#include "custom_api.h"
Note:此外,为了与C语言兼容,C++在将原有C语言头文件标准化后,头文件前会带有c
字母,如cstdio
、cstring
、ctime
、ctype
等等。于是,当我们要用C++标准化了的C语言头文件时,可以进行如下的转换:
#include <stdio.h> --> #include <cstdio>
#include <stdlib.h> --> #include <cstdlib>
#include <string.h> --> #include <cstring>
使用#ifndef & #define & #endif
防止声明冲突
使用#ifndef & #define & #endif
可以防止重复声明,这是因为如果不使用文件标识符的话,多次引用相同头文件会重复声明某些相同的字段,而引起冲突;
# pragma once
:对"#ifndef & #define & #endif"的升级宏定义
1.2 Namespace:命名空间
Namespace表示某些工具集合的作用域;
“作用域前面加::”:显式调用全局域关键字
关于“C++中‘作用域前面加::’做法的用处”,请参考知乎回答《C++中“作用域前面加::”:可以减少重名的可能性》
简单来说,其目的就是可以显式指定:准备调用的关键字符号是来自于全局空间global,这样编译器则会从全局空间开始检索该符号;
我们用一个简单的demo来验证这一点:
#include <iostream>
using namespace std;
int a = 3;
int main()
int a = 1;
cout<<a<<endl;
cout<<::a;
>>>
1
3
// 可以看到使用global指示的变量是全局变量a,所以输出值为3
1.3 Const:常量修饰符
关于const
的作用,我感觉菜鸟教程的解释是很好理解的,这里引用一下:
C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
2. Data Type
2.1 常见数据类型
关于常见的扩展数据类型,请参考博文《c++基础之uint8_t》
uint8_t
: unsigned char,无符号字符类型。
3. Data Structure
Array: []
Auto数组引用:auto &a
List: std::list
关于list
的初始化,请参考《在 C++ 中初始化一个 std::list》
Vector: std::vector
std::vector
可以同时支持索引访问和迭代访问;
Note
vector
内部是使用array实现的,在进行中间插入时速度较慢,如果需要频繁地进行中间插入,可以考虑使用list
。
4. Function
4.1 类型说明符
Consteval:将可推导结果函数代码编译为立即数
Prerequisite: gcc >= 10.1
Explicit:停止函数使用隐式转换和复制初始化
关于explicit的作用讲解,请参考博文《C++ explicit 关键字》
4.2 形式参数
形参模板:
const auto& param
5. 类
class 派生类名: 继承方式(访问说明) 基类名1, 继承方式 基类名2, ..., 继承方式 基类名n
派生类成员声明
;
Note
关于访问说明对继承基类成员的影响,请参考文章《C++ 公共、受保护和私有继承》。
5.2 析构函数
5.2.1 基类析构函数
virtual ~Base() = default;
写作说明
virtual
:当存在子类时,如果使用父类标记接收了子类变量,在析构时,标记自动寻找子类的析构函数并执行,不然只对父类成员进行析构。
= default
:此写法跟在使用上几乎没有明显的区别,不过可读性会更清晰一些。
5.2.2 函数关键字:delete
delete
的作用是删除函数的调用入口;
Delete “复制构造函数” (copy constructor)
Delete “移动构造函数” (move constructor)
Make class instance not copyable and not movable
我们在dali-doc中看到这样的代码:
Dummy(const Dummy&) = delete;
Dummy& operator=(const Dummy&) = delete;
Dummy(Dummy&&) = delete;
Dummy& operator=(Dummy&&) = delete;
其目的是使得Dummy算子实例无法被复制或者转移;
经过查询资料,我们知道在以下场景中可能出现这种情况:
(一)所有权不允许转移:
This class has all copy and move constructors and assignments deleted which makes it not copyable and not movable. As for why I can’t speak of because I am not familiar with the lib, but presumably it models some form of unique non-transferable ownership.
(二)保证资料引用的唯一性(单例模式)
Objects sometimes represent actual resources (or ownership) of resources of which there can be only one. Copying those instances would not be right. Deleting the move constructor (and assignment) clearly models that you can’t transfer ownership to some other bit of code (and you want to keep control over the objects lifecycle at all the time).
函数覆写:override
模板:
bool ActualFunc() const override
return true;
写作说明:
override
:这里使用override显式告知编译器此函数是对基类函数的覆写;
const
:显式告知编译器此函数是非原位修改式的函数。
6. 常用工具库
7. CMake编译
7.1 Project:指定项目名称和语言类型
命令格式:project(<PROJECT-NAME> [<language-name>...])
Note
项目名称<PROJECT-NAME>
不需要与项目根目录名称相同。
Project示例
project(custom_dummy_plugin)
:指定了工程名字;
project(custom_dummy_plugin CUDA CXX C)
:指定了工程名字,并且支持语言是C和C++;
5.2 SET:定义变量(值和列表)
set(CMAKE_CXX_STANDARD 17)
:定义变量 CMAKE_CXX_STANDARD的值为17
5.3 Message:向终端输出用户自定义的信息
主要包含三种信息:
- SEND_ERROR:产⽣错误,⽣成过程被跳过。
- SATUS:输出前缀为
—
的信息。 - FATAL_ERROR:⽴即终⽌所有CMake过程。
5.4 在launch.json中配置cmake
CMake Tools
插件教程:《VSCode-cmake-tools/debug-launch.md | Debug using a launch.json file》
// Resolved by CMake Tools:
"program": "$command:cmake.launchTargetPath",
5.5 在CMake中寻找python解释器
关于使用CMake命令寻找python解释器,请参考博文《C++ clion使用python》
Troubleshooting
(1)出现:The CUDA compiler identification is unknown…
有一次在编译时,出现这样的错误:
[Bash]: The CUDA compiler identification is unknown…
[CMakeError.log]:
Checking whether the CUDA compiler is NVIDIA using “” did not match “nvcc: NVIDIA (R) Cuda compiler driver”:
Checking whether the CUDA compiler is Clang using “” did not match “(clang version)”:
Compiling the CUDA compiler identification source file “CMakeCUDACompilerId.cu” failed.
Compiler: CMAKE_CUDA_COMPILER-NOTFOUND
Build flags:
Id flags: -v
The output was:
No such file or directory
Compiling the CUDA compiler identification source file “CMakeCUDACompilerId.cu” failed.
Compiler: CMAKE_CUDA_COMPILER-NOTFOUND
Build flags:
Id flags: -vThe output was:
No such file or directory
Checking whether the CUDA compiler is NVIDIA using “” did not match “nvcc: NVIDIA (R) Cuda compiler driver”:
Checking whether the CUDA compiler is Clang using “” did not match “(clang version)”:
Compiling the CUDA compiler identification source file “CMakeCUDACompilerId.cu” failed.
Compiler: CMAKE_CUDA_COMPILER-NOTFOUND
Build flags:
Id flags: -vThe output was:
No such file or directory
…
从网上的资料来看,似乎是CMake未能找到CUDA编译器,我们尝试过手动指定CUDA的编译器,有时会修复这个问题:
set(CMAKE_CUDA_COMPILER "/usr/local/cuda/bin/nvcc")
8. 代码调试
8.1 IO输出调试:输出重定向
在《第八期-C++基础与深度解析》课程中,老师使用了“输出重定向”的语句来查看cout
和cerr
的结果:
./HelloWorld >txt1 2>txt2
代码含义:将程序HelloWorld的标准输出stdout重定向至文件txt1,将标准错误输出stderr定向至文件txt2。
Note
这里“2>”中的“2”指的是stderr文件描述索引号2。
8.2 Compiler Explorer:可以查看编译生成的汇编代码
8.1.1 界面介绍
打开汇编分析器
操作路径:C++ source
→ Add new...
→ Compiler
;
8.2 VSCode调试
8.2.0 Variable substitution:变量代号
官方文档:Visual Studio Code Variables Reference
8.2.1 Launch.json:运行配置
Auto-generated demo
"version": "0.2.0",
"configurations": [
"name": "C/C++: g++ 生成和调试活动文件",
"type": "cppdbg",
"request": "launch",
"program": "$fileDirname/$fileBasenameNoExtension",
"args": [],
"stopAtEntry": false,
"cwd": "$fileDirname",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerArgs": "-q -ex quit; wait() fg >/dev/null; ; /usr/bin/gdb -q --interpreter=mi",
"setupCommands": [
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
,
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
],
"preLaunchTask": "C/C++: cpp 生成活动文件",
"miDebuggerPath": "/usr/bin/gdb"
,
// the second config...
]
name:配置名称
name
字段用来设置当前配置的名称,会出现在运行设置按钮的下拉列表中,不过目前并没有发现name
会作为配置项的唯一标识符;
type:编译器类型
System | type-string |
---|---|
Ubuntu | cppdbg |
Windows | cppvsdbg |
官方文档:Configure launch.json for C/C++ debugging in VSCode | type
request:指令类型
request | type-string |
---|---|
launch | launch the program |
attach | to attach a running instance |
additionalSOLibSearchPath:搜索.so
文件的路径
告诉GDB或LLDB要在哪些路径中搜索.so
文件。使用分号;
来分隔多个路径。例如:/Users/user/dir1;/Users/user/dir2
。
externalConsole:使用外部控制台
当声明为true
时,表示产生一个外部控制台来运行程序。
8.2.2 Tasks.json:任务配置
Auto-generated demo
"tasks": [
"type": "cppbuild",
"label": "C/C++: cpp 生成活动文件",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"$file",
"-o",
"$fileDirname/$fileBasenameNoExtension"
],
"options":
"cwd": "$fileDirname"
,
"problemMatcher": [
"$gcc"
],
"group":
"kind": "build",
"isDefault": true
,
"detail": "调试器生成的任务。"
,
// the second task...
],
"version": "2.0.0"
label:标签标识符
label
关键字是当前task的唯一标识符(ID),用来区别不同的task。
options:设置环境选项,cwd, env, shell
官方文档:Tasks in Visual Studio Code | options
示例:
"options":
"cwd": "$fileDirname"
表示将工作目录设置为当前运行文件所在的目录。
problemMatcher:错误匹配器
VSCode-C/C++插件已经实现用于C++调试的problemMatcher,我们直接使用"$gcc"
即可。
9. 编译优化:-O3
我们可以配置编译器选项来对代码进行编译优化;
10. Python使用C++扩展联合调试
我们目前计划了三种调试方式:
- VSCode联合调试:使用插件 Python C++ Debugger;
- Visual Studio: Visual Studio可以支持WSL2系统;
- TotalView:TotalView支持Python&C++联合调试;
关于VSCode使用GDB进行联合调试的方法,请参考知乎文章《编译、调试PyTorch源码》
10.1 VSCode联合调试
关于在VSCode中调试python-C++代码,请参考《vscode python/C++ Debug 调试 Pytorch源码》
Python C++ Debugger
主要支持对调用C++共享链接库的python代码的调试;
Demo of launch.json
on Ubuntu
"version": "0.2.0",
"configurations": [
"name": "Python C++ Debug",
"type": "pythoncpp",
"request": "launch",
"pythonLaunchName": "Python: Current File",
"cppAttachName": "(gdb) Attach",
,
"name": "(gdb) Attach",
"type": "cppdbg",
"request": "attach",
"processId": ""
,
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "$file",
"console": "integratedTerminal"
]
11. [示例]: Dali算子类声明
class Dummy : public ::dali::Operator<Backend>
// 这里派生类Dummy以public形式继承dali::Operator<Backend>
public:
inline explicit Dummy(const ::dali::OpSpec &spec) :
::dali::Operator<Backend>(spec)
// explicit: 指定构造函数Dummy()为显式调用
virtual inline ~Dummy() = default;
Dummy(const Dummy&) = delete;
Dummy& operator=(const Dummy&) = delete;
Dummy(Dummy&&) = delete;
Dummy& operator=(Dummy&&) = delete;
protected:
bool CanInferOutputs() const override
return true;
bool SetupImpl(std::vector<::dali::OutputDesc> &output_desc,
const ::dali::workspace_t<Backend> &ws) override
const auto &input = ws.template Input<Backend>(0);
// ws.template:显式告知编译器指定后面的Input<Backend>(0)是函数模板
output_desc.resize(1);
output_desc[0] = input.shape(), input.type();
return true;
void RunImpl(::dali::workspace_t<Backend> &ws) override;
;
以上是关于C++ Cookbook by Eric的主要内容,如果未能解决你的问题,请参考以下文章