记录一些遇到的cpp新特性或者实现技巧上的问题。

变长参数

我记得三年前我还好好学过,现在都忘光了,重新整理一下:

基本定义

/**
* @brief 获取不同长度的可变参数,打印参数的个数
*
* @tparam T
* @param args
*/
template <typename... T>
void func(T... args) {
cout << sizeof...(args) << endl;
}

TEST(test_varg, basic) {
func();
func(1);
func(1, 2);
func(1, 2, '3', vector<int>{3});
}
/**
[ RUN ] test_varg.basic
0
1
2
4
[ OK ] test_varg.basic (0 ms)
*/

循环展开

错误写法

参数匹配的时候需要注意,多个参数输入,最后会匹配到单参数的输入,但是单参数的时候不会调用无参数的终止函数。

void print() { cout << " ;" << endl; }
template <typename T>
void print(T v) {
cout << v << " ";
}
template <typename T, typename... Args>
void print(T head, Args... args) {
print(head);
cout << "remain size : " << sizeof...(args) << endl;
print(args...);
}

TEST(test_varg, recursive_print_error_version) {
print();
print(1, 2);
print(1, 2.3, '3');
}
/**
[ RUN ] test_varg.recursive_print
;
1 remain size : 1
2 1 remain size : 2
2.3 remain size : 1
3 [ OK ] test_varg.recursive_print (0 ms)
*/

正确写法

所以我们通常用一个无参的函数来终止,在多参数的函数中调用单个参数的处理方式。下面的例子就展示了正确的log函数以及一个从任意base累加的函数。

void ic() { cout << " ; " << endl; }
template <typename T, typename... Args>
void ic(T head, Args... args) {
cout << head << " ";
ic(args...);
}

template <typename T>
T sums() {
return (T)(-2);
}
template <typename T, typename... Ts>
T sums(T v, Ts... vs) {
return v + sums<T>(vs...);
}

TEST(test_varg, recursive_print_right_version) {
ic(sums(1, 2));
ic(sums(1, 2.3, 3.5));
ic(sums(2.3, 1, 3.5));
}
/*
[ RUN ] test_varg.recursive_print_right_version
1 ;
4 ;
4.8 ;
[ OK ] test_varg.recursive_print_right_version (0 ms)
*/

变参模板展开

这里利用c++17中的特性方便的展开并对参数进行处理。


template <typename T>
int toint(T t) {
return (int)t;
}

template <typename... Args>
vector<int> toints(Args... args) {
vector<int> arr = {toint(args)...};
return arr;
}
// 利用if constexpr编译期间即构造出对应的输出方式。
template <typename Head, typename... Args>
void print2(Head head, Args... args) {
cout << head;
if constexpr (sizeof...(args) > 0) {
cout << " , ";
print2(args...);
} else {
cout << " ; " << endl;
}
}

TEST(test_varg, expand_right_version) {
auto arr = toints(1.2, 2.4, 3.6, 4.7);
for (size_t i = 0; i < arr.size(); i++) { ic(arr[i]); }
auto arr2 = countargs(1.2, 2.4, 3.6, 4.7);
for (size_t i = 0; i < arr2.size(); i++) { ic(arr2[i]); }
print2("hello", "word", "yes", 1, 3, 4.5);
}


我们可以利用一个函数对每个参数进行操作,并且结合逗号表达式可以做一些别的事情。
[ RUN      ] test_varg.expand_right_version
1 ;
2 ;
3 ;
4 ;
0 ;
1 ;
2 ;
3 ;
hello , word , yes , 1 , 3 , 4.5 ;
[ OK ] test_varg.expand_right_version (0 ms)
### 折叠表达式

用折叠表达式可以把一些简单的递归写的更加简洁,但是他的运算方向和人通常认为的不同:

template <typename... T>
auto rsub(T... t) {
return (t - ...);
}
template <typename... T>
auto lsub(T... t) {
return (... - t);
}

TEST(test_varg, fold_expressions) {
cout << rsub(1, 2, 3, 4, 5) << endl;
cout << lsub(1, 2, 3, 4, 5) << endl;
}
[ RUN      ] test_varg.fold_expressions
3
-13
[ OK ] test_varg.fold_expressions (0 ms)

integer_sequence

利用integer_sequence我们可以展开一些数组,并对数组中的每个元素做处理,下面就给出一段编译期展开循环去计算卷积的程序:

template <typename T, size_t... W>
void conv1xM(float& sum, T r, T k, std::index_sequence<W...>) {
((sum += r[W] * k[W]), ...);
}

template <size_t Filter_W, typename T, size_t Filter_H, size_t... H>
void convNxM(float& sum, std::array<T, Filter_H>& r, std::array<T, Filter_H>& k,
std::index_sequence<H...>) {
(conv1xM(sum, r[H], k[H], std::make_index_sequence<Filter_W>{}), ...);
}

template <size_t Filter_W, typename T, size_t Filter_H>
void convNxM(float& sum, std::array<T, Filter_H>& r,
std::array<T, Filter_H>& k) {
convNxM<Filter_W>(sum, r, k, std::make_index_sequence<Filter_H>{});
}

TEST(test_tmp, conv) {
constexpr size_t K_h = 3, K_w = 6;
size_t I_h = 32, I_w = 64;
float kernel[K_h * K_w];
std::iota(kernel, kernel + K_h * K_w, 0);
float image[I_h * I_w];
std::iota(image, image + I_h * I_w, 0);
IC(image[0]);

std::array<float*, K_h> r{image, image + I_w, image + I_w * 2};
std::array<float*, K_h> k{kernel, kernel + K_w, kernel + K_w * 2};
float sum = 0.;
convNxM<K_w>(sum, r, k);
IC(sum);
}

输出

[ RUN      ] test_tmp.conv
ic| image[0]: 0
ic| sum: 14835
[ OK ] test_tmp.conv (0 ms)

总之我们利用模板传递静态时期的常量,然后利用index sequence对当前的数组进行索引从而获得展开循环的加速效果。

右值&左值

一个问题

void dosomething(vector<int>& arr) { arr[2] = 0; }

TEST(rvalue, basic) {
vector<int> arr{1, 3, 4};
dosomething(arr);
dosomething(vector<int>{1, 3, 4});
}

模板

获取容器内部类型

有个很蛋疼的问题就是对于传入一个array的数据,我们的模板定义需要

template<typename T,size_t N>
然后我就找了一下有没有方便的办法,发现可以这样:
template <typename Array>
using element_type_t =
std::remove_reference_t<decltype(*std::begin(std::declval<Array&>()))>;

template <size_t N = 20, typename Array>
auto get_same_type_array(Array& a, element_type_t<Array> b) {
std::array<element_type_t<Array>, N> c{b};
return c;
}

TEST(test_tmp, test_get_array_type) {
constexpr size_t K_h = 3, K_w = 6;
size_t I_h = 32, I_w = 64;
float kernel[K_h * K_w];
std::iota(kernel, kernel + K_h * K_w, 0);
float image[I_h * I_w];
std::iota(image, image + I_h * I_w, 0);
IC(image[0]);

std::array<float*, K_h> r{image, image + I_w, image + I_w * 2};
std::array<float*, K_h> k{kernel, kernel + K_w, kernel + K_w * 2};
auto b = get_same_type_array<5>(r, image);
IC(b);
}
实际上也就是静态时期的编译获得类型,然后得知输入b的类型,这样我们少输入一个模板,在多重模板迭代的过程中十分方便。

输出:

[ RUN      ] test_tmp.test_get_array_type
ic| image[0]: 0
ic| b: [0x16ceccb30, 0x0, 0x0, 0x0, 0x0]
[ OK ] test_tmp.test_get_array_type (0 ms)

编译期通过类型执行不同的代码

class Mat {
public:
float* data;
Mat() { data = new float[100]; }
~Mat(){};
};

template <typename T>
void make_mat(T& m) {
if constexpr (std::is_pointer<T>()) {
IC("is pointer");
m[0] = 100;
} else {
IC("is Mat");
m.data[0] = 100;
}
}

TEST(test_tmp, integral_constant) {
Mat m1;
float* m2 = new float[10];
make_mat(m1);
make_mat(m2);
}

输出

[ RUN      ] test_tmp.integral_constant
ic| std::string("is Mat"): "is Mat"
ic| std::string("is pointer"): "is pointer"
[ OK ] test_tmp.integral_constant (0 ms)

类型保存值

这个是我写了半天才发现c++模板的套路,其实就是把一个变量看成一个类型,下面就是把这个模板参数用两种方式表示(不过我暂时还不知道如何选择这两种方式):

template <uint64_t mmu_item,
uint64_t start_bank,
MMU_CONF_WIDTH width,
uint64_t start_depth,
uint64_t depth>
struct inst_mmu_conf_warper
{
using type = std::index_sequence<0x12, mmu_item,
start_bank,
static_cast<uint64_t>(width),
start_depth,
depth>;
constexpr static auto values = std::index_sequence<0x12, mmu_item,
start_bank,
static_cast<uint64_t>(width),
start_depth,
depth>{};
};

结构体模板元函数的套路

其实一开始写模板看不懂就是因为c++的语法太多了,不过我们还是只需要掌握一些主要的语法就可以了.

我主要接触到的模板主要分以下几种表示方法.

/* 模板元函数的写法 1 */

// 定义元函数的入参,这里表明这个结构体接收一个类型作为参数
template <typename T>
struct method_1 {};

// 我们特化上面的那个元函数,通常特化直接写值,但是由于我们当前给的参数还依赖一个未知的`Value`,因此还需要给元函数再加一个模板类型.
template <size_t Value>
struct method_1<std::integral_constant<size_t, Value>>
{
// 同时对于这个模板元的返回值也有两种方法,可以是一个静态的变量,也可以是对应的类型(此时那个类型其实也保存了值)
constexpr static size_t one_v = Value + 1;
using one_t = std::integral_constant<size_t, Value + 1>;
};

模板元函数编写与匹配的几个套路

在c++17之前我们可以用std::enable_if来决议这个匹配是不是有效的,首先对于同一个函数,他的返回值应该是一致的(保证我们思维的一致性),所以通过enable_if决议当前的输入类型下是否可以匹配. 下面这个例子就是把整形和array类型通过决议分开匹配,从而实现不同的assgin_to_array,不然输入的array还是会被默认匹配到第一个函数:

template <typename T, size_t... Is>
constexpr std::enable_if_t<std::is_integral_v<T>, void> assgin_to_array(std::array<uint8_t, sizeof(T)> &dest, const T &src, std::index_sequence<Is...>)
{
((dest[Is] = (src >> (Is * 8))), ...);
}

template <typename T1, typename T2, size_t N, size_t... Is>
constexpr std::enable_if_t<sizeof(T1) == sizeof(T2), void> assgin_to_array(std::array<T1, N> &dest, const std::array<T2, N> &src, std::index_sequence<Is...>)
{
copy(dest.data(), src.data(), 0, std::index_sequence<Is...>{});
}

不过现在有了constexpr if,我们可以直接在同一个函数中不同的操作,这里要注意一个小坑,就是std::is_array只能检测是不是c风格的数组,但是他不能检测std::array,所以我这里重写了一个is_std_array:

template <typename T, size_t... Is>
constexpr void assgin_to_array(std::array<uint8_t, sizeof(T)> &dest, const T &src, std::index_sequence<Is...>)
{
if constexpr (std::is_integral_v<T>)
{
((dest[Is] = (src >> (Is * 8))), ...);
}
else if constexpr (is_std_array<T>::value)
{
copy(dest.data(), src.data(), 0, std::index_sequence<Is...>{});
}
}

tuple元素进行fold expressions中重载操作符遇到的坑

我想重载+然后对tuple进行操作,但是遇到找不到重载加号的问题.最后发现这个操作必须要声明到std才有效.

变长模板匹配的模式

c++变长模板他不能匹配seq<Le..., Ls>这种模式,只能匹配seq<Ls,Le...>,也就是取第一个元素是方便的(或者前n个元素都是方便的),如果你想取最后一个元素那么就需要递归一次.

template <typename T>
struct take_head
{
};

template <size_t Ls, size_t... Le>
struct take_head<seq<Ls, Le...>>
{
constexpr static size_t value = Ls;
};

template <typename A, typename B>
struct take_two_head
{
};

template <size_t Ls, size_t... Le, size_t Rs, size_t... Re>
struct take_two_head<seq<Le..., Ls>, seq<Re..., Rs>>
{
constexpr static size_t value_L = Ls;
constexpr static size_t value_R = Rs;
};

TEST(test, take_two_head)
{
take_two_head<seq<0, 1, 2, 3, 4>, seq<5, 6>> two{};
// auto lh = {} value;
ic(two.value_L);
ic(two.value_R);
}

通过隐式转换来实现零成本抽象

通过vector包


#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <numeric>
#include <optional>
#include <span>
#include <utility>
#include <arm_neon.h>
#include <iostream>

template <class T, size_t... Lanes> struct native_vector_type;

template <> struct native_vector_type<float, 32> {
using type = float32x4_t[8];
};

template <> struct native_vector_type<float, 4> {
using type = float32x4_t;
};


template <class T, size_t... Lanes> struct vector {
using element_type = T;
using value_type = typename native_vector_type<T, Lanes...>::type;

private:
alignas(sizeof(value_type)) value_type v_;

public:
vector() = default;

vector(const value_type &vec) : v_(vec) {}

constexpr operator value_type() const noexcept { return v_; }

constexpr operator value_type &() noexcept { return v_; }

constexpr auto buffer() noexcept {
return std::span(reinterpret_cast<element_type *>(&v_),
(1 * ... * Lanes));
}

};

int main()
{
vector<float,4> *a = new vector<float,4>();
a->buffer()[0] = 1.0f;
a->buffer()[1] = 2.0f;
vector<float,4> b;
b.buffer()[0] = 1.0f;
b.buffer()[1] = 2.0f;
// float32x4_t d;
vector<float,4> c = *a * b;
std::cout << c.buffer()[0] << std::endl;
std::cout << c.buffer()[1] << std::endl;
}

explicit

explicit是用于表示当前的构造函数不能进行隐式转换,比如如下代码:

class StrBlob {
public:
shared_ptr<vector<string>> data;

StrBlob() : data(make_shared<vector<string>>()){};
StrBlob(initializer_list<string> il)
: data(make_shared<vector<string>>(il)){};
size_t size() { return data->size(); };
size_t use_count() { return data.use_count(); };

const string& front() {
check(0, "front");
return data->front();
}
const string& back() {
check(0, "back");
return data->back();
}
void push_back(const string& s) { data->push_back(s); }
void pop_back() {
check(0, "pop_back");
data->pop_back();
}

void combine(StrBlob sb) {
for (auto p = sb.data->begin(); p != sb.data->end(); p++)
data->push_back(*p);
}

private:
void check(size_t i, const string& msg) const {
if (i >= data->size()) { throw out_of_range(msg); }
}
};

TEST(test, t_12_5) {
StrBlob b1{"1", "2", "3"};
ic(*b1.data);
b1.combine({"4", "5", "6"});
ic(*b1.data);
}

我们在调用combine函数的时候需要的是一个StrBlob对象,但是我们实际combine的时候传入的是一个初始化列表,那么这个时候就会把隐式调用对应的构造函数,完成参数传递。这个时候如果我们给对应初始化列表的构造函数进行explicit限制,那么就会出现编译错误,要求传入一个正确的StrBlob对象。

share_ptr

构造函数

这里有点奇怪,不强行指定initializer_list<int>是无法通过编译的,这太蛋疼了。

make_shared<vector<int>>(initializer_list<int>{1, 2, 3})
同时make_shared还不支持new的方式构造shared_ptr。 ## 拷贝赋值

他的赋值是直接把被赋值对象的内容给清空了,被赋值之后,q和 p其实都是q了,最后退出scope的时候,p的use_count会因为q的释放减少。

TEST(test, shared_ptr_copy) {
auto p = make_shared<int>(10);
{
auto q = make_shared<int>(11);
p = q;
ic(*q, q.use_count());
ic(*p, p.use_count());
}
ic(*p, p.use_count());
}

[ RUN      ] test.shared_ptr_copy
ic| *q: 11, q.use_count(): 2
ic| *p: 11, p.use_count(): 2
ic| *p: 11, p.use_count(): 1
[ OK ] test.shared_ptr_copy (0 ms)

C++: munmap_chunk(): invalid pointer

  1. 通常这个问题应该是delete的内存不是被new出来的。
  2. 指针运行时被修改
  3. 指针越界(数组越界赋值了,但是当时不报错)

clang编译中报错 no viable overloaded '='

这个应该是clang的map头文件实现导致的,

_LIBCPP_INLINE_VISIBILITY
__value_type& operator=(const __value_type& __v)
{
__ref() = __v.__get_value();
return *this;
}

通过clangd的类型提示发现:

public: std::__value_type::value_type &__get_value()
no viable overloaded '='GCC
__tree(1662, 39): in instantiation of member function 'std::__value_type<const air::Var, int>::operator=' requested here

这个应该就是声明了一个包含const air::Var类型的map,然后类型特化的时候就出错了.

ld: unknown option: -z

这个也是在mac上独有的问题,gcc ld中可以通过-Wl,-z,relro,-z,now,-z,noexecstack传递linker的参数,但是mac上虽然也可以给apple clang提供相同的命令行,但是真正调用ld的时候就会报上述错误,原因就是mac上的ld真的不支持这些选项...所以关闭即可.

ld: unknown CoreFoundation

我想给target加上link参数,但是用cmake的写法发现有个大坑, 如果直接加Wl然后选项, 再用字符串包裹起来, 是无法编译的, 但是如果不用字符串, 编译器就会把空格后面的东西识别成另一个选项.

target_link_options(simulator_k510 PUBLIC "-Wl,-framework CoreFoundation")
所以正确做法是连续的加Wl
target_link_options(simulator_k510 PUBLIC -Wl,-framework -Wl,CoreFoundation)

m1上eigen使用fp16时的坑

在m1上eigen会检测到有fp16计算单元,此时__half_raw结构体中包含的就是__fp16类型

struct __half_raw {
#if defined(EIGEN_HAS_ARM64_FP16_SCALAR_ARITHMETIC)
explicit EIGEN_DEVICE_FUNC EIGEN_CONSTEXPR __half_raw(numext::uint16_t raw) : x(numext::bit_cast<__fp16>(raw)) {
}
__fp16 x;
#else
explicit EIGEN_DEVICE_FUNC EIGEN_CONSTEXPR __half_raw(numext::uint16_t raw) : x(raw) {}
numext::uint16_t x;
#endif
}
如果没有那么就是标准的uint16类型,所以此时不能随便将__fp16x赋值给fp16,需要使用Eigen::numext::bit_cast<uint16_t>(dst_eig_f16.x)才行.

c语言regex使用

c里面的正则库是一个非常老的库,所以他的正则语法和其他语言有一部分正好相反,需要参考这里.

linux下使用shared memory的坑

我是在docker中的ubuntu容器中使用shared memory, 发现虽然可以创建shm文件,但是只要进行内存写入copy的时候就会报错Bus Error. 参考这篇文章才知道是docker本身限制的容器的shared memory大小只有64M.

df -h | grep shm
shm 64M 64M 0 100% /dev/shm

命令行调试dotnet core的dump信息

  1. 安装dotnet sos
  2. 然后开启core dump收集
    ulimit -c unlimited
    echo "core.%e.%p" > /proc/sys/kernel/core_pattern
  3. 通过lldb挂载core file

这里我直接用vscode插件里面的lldb, 这个版本比较新,好用:

~/.vscode-server/extensions/vadimcn.vscode-lldb-1.7.2/lldb/bin/lldb -c tests/ForDebug/core..NET\ ThreadPool.298375

然后进去输入bt检查栈内容, 反正一顿调之后, 终于发现问题出在了.net过早的释放了c++的类对象. 原因是这个.net中类是对interpreter的包装,然后再构造entry funciton的对象, entry function里面包含了interpreter, 但是在调用entry function invoke的时候.net还是会把interpreter释放掉, 然后c++中就会出现runtime function运行到一半时访问了被释放的自身. 上面这个问题也只有在.net的release中才会出现, 我怀疑是因为.net优化了

mac中缺少gmp的问题

mac中安装clang时候默认是没有gmp.h的, 但是如果安装了python的话是可以在python的inclue中找到.

❯ ll /Users/lisa/miniforge3/envs/dl/include | grep gmp
-rw-rw-r-- 1 lisa staff 82K 5 15 18:32 gmp.h
-rw-rw-r-- 2 lisa staff 126K 11 19 2020 gmpxx.h

然后可以添加链接库和头文件位置:

CFLAGS=-I/Users/lisa/miniforge3/envs/dl/include
LDFLAGS=-L/Users/lisa/miniforge3/envs/dl/lib

mac中编译出现libclang-cpp.dylib的问题

我这里是编译pet的lib, 然后make install的报错如下:

Reason: tried: '/usr/local/lib/libclang-cpp.dylib' (no such file), '/usr/lib/libclang-cpp.dylib' (no such file)

然后发现是因为他的makefile里面调用了之前链接libclang的可执行文件extract_interface, 但是不知道为什么他的rpath没有设置对,导致还得手动设置LD_LIBRARY_PATH才能找到.so. 然后尝试了一下外部export环境变量,但是在makefile中并不起作用, 所以直接改了makefile: NOTE : 我这里是mac所以使用DYLD_LIBRARY_PATH

isl.py: libdep.a interface/isl.py.top
(cat $(srcdir)/interface/isl.py.top && \
DYLD_LIBRARY_PATH=/Users/lisa/Documents/llvm-project/build/install/lib/ isl/interface/extract_interface$(EXEEXT) \
--language=python \
$(DEFAULT_INCLUDES) -I$(top_srcdir)/isl/include -I$(top_builddir)/isl/include \
"./isl/all.h") \
> $@ || (rm $@ && false)

找到了根本原因,其实就是他的编译器选项和clang的不同,他使用了-R来声明rpath, 但是clang中要使用-Wl,-rpath,xxx来设置rpath:

CLANG_RFLAG=`echo "$CLANG_LDFLAGS" | $SED -e 's\/-L\/-R\/g'`
# 改成如下即可.
CLANG_RFLAG=`echo "$CLANG_LDFLAGS" | $SED -e 's/-L/-Wl,-rpath,/g'`

mac中'stdio.h' file not found

sudo ln -s /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/* /usr/local/include/

relocation R_RISCV_JAL out of range

好像是因为.a太大了的缘故,导致relocation跳转不过去.

[build] ld: error: /home/zqh/workspace/AndeSight_STD_v323/toolchains/nds64le-elf-mculib-v5d/bin/../lib/gcc/riscv64-elf/7.4.0/../../../../riscv64-elf/lib/libc.a(lib_a-memmove.o):(function memmove): relocation R_RISCV_JAL out of range: 645800 is not in [-524288, 524287]
[build] ld: error: /home/zqh/workspace/AndeSight_STD_v323/toolchains/nds64le-elf-mculib-v5d/bin/../lib/gcc/riscv64-elf/7.4.0/../../../../riscv64-elf/lib/libc.a(lib_a-memmove.o):(function memmove): relocation R_RISCV_JAL out of range: 645718 is not in [-524288, 524287]

lldb 调试python/c++/csharp

  1. 添加各种环境变量.
  2. 用lldb启动python ❯ lldb-10 /root/miniconda3/envs/ci/bin/python
  3. 进入后通过run 启动py文件 (lldb) run test_mobilenetv1.py