风停了,我没有停,我不会怕,我还有我自己

源代码在: https://github.com/archibate/archibate.github.io/tree/master/codes/invoke-c-shared-library-from-python

最近需要在 Blender 里调用 Zeno 的 C++ 部分,本来一直在用 Pybind11,但是他一旦换一个 Python 版本就会加载失败,同时一个个去调用 def 也非常麻烦。
目前主要是传指针字符串之类的,没有对复杂数据结构和类的深度绑定的需求,因此我开始探索另一种兼容性更好的方案——甚至不需要 Python 的头文件。

原来 Python 支持导入任意由 C 语言编写的动态链接库(.so 或者 .dll),并调用其中的函数。
这其中又有哪些坑呢,今天就让我们一探究竟。

符号导出机制

针对不同的系统,分类讨论:

Linux 很简单

首先用 CMake 创建一个 shared library 目标:

1
2
# CMakeLists.txt
add_library(mylib SHARED mylib.c)

C 语言源代码如下:

1
2
3
4
5
6
// mylib.c
#include <stdio.h>

void say_hello() {
printf("Hello, world!\n");
}
1
2
cmake -B build
cmake --build build

编译后会得到 build/libmylib.so

然后在 Python 脚本里写:

1
2
3
4
import ctypes

mylib = ctypes.cdll.LoadLibrary('build/libmylib.so')
mylib.say_hello()

成功打印:

1
Hello, world!

Windows 不一样

然而我们试着如果用同样的配置在 Windows 上测试:

1
2
3
4
import ctypes

mylib = ctypes.cdll.LoadLibrary('build\\mylib.dll')
mylib.say_hello()

会发现调用出错:

1
AttributeError: build\mylib.dll: undefined symbol: say_hello

这是什么原因呢?

原来 Windows 的设计为了安全,默认不会把 DLL 的所有符号导出,需要这样写:

1
2
3
4
5
6
7
8
// mylib.c
#include <stdio.h>

__declspec(dllexport) void say_hello() {
printf("Hello, world!\n");
}

void this_func_wont_export() {}

需要导出的,前面加上 __declspec(dllexport),不希望导出的,就不加。
如果想要和 Linux 一样默认全部导出,也可以在 CMake 里这样指定:

1
2
3
4
# CMakeLists.txt
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

add_library(mylib SHARED mylib.c)

Linux 也可以默认不导出

反过来,Linux 也可以修改成默认不导出,只有指定的符号才导出:

1
2
3
4
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)

add_library(mylib SHARED mylib.c)

然后像这样指定前缀:

1
2
3
4
5
6
7
8
// mylib.c
#include <stdio.h>

__attribute__((visibility("default"))) void say_hello() {
printf("Hello, world!\n");
}

void this_func_wont_export() {}

我们可以用一个宏来统一 Windows 和 Linux 的导出指定前缀:

1
2
3
4
5
6
7
8
9
#ifdef _WIN32  // 如果在 Windows 上
#define DLLEXPORT __declspec(dllexport)
#else // 否则在 Unix 类系统上
#define DLLEXPORT __attribute__((visibility("default")))
#endif

DLLEXPORT void say_hello() {
printf("Hello, world!\n");
}

C++ 大不一样

然而,如果我们用的不是 C 语言,而是 C++,还是会出现符号找不到的问题:

1
AttributeError: build/mylib.so: undefined symbol: say_hello

这是为什么呢?我们来用 Linux 下的 nm 小工具分析一下生成的动态链接库。

用 C 语言编译:

1
2
$ nm build/libmylib.so | grep -w T
0000000000001109 T say_hello

用 C++ 编译:

1
2
$ nm build/libmylib.so | grep -w T
0000000000001139 T _Z9say_hellov

可以看到 C++ 中原本叫 say_hello 的函数变成了一个奇怪的名字: _Z9say_hellov
这是为什么呢?

C++ 函数名重组机制

原来是 C++ 为了实现函数的重载和名字空间等特性,对函数名对应的符号进行了一些魔改,
C++ 魔改的符号都以 _Z 开头,后面紧跟着一个数字,表示接下来的符号长度,这里
say_hello 的字符串长度为 9,因此是 _Z9,然后 v 表示函数的参数是 void,也就
是没有参数。

解决方法是要么直接在 Python 里写重组后的符号名:

1
2
3
4
import ctypes

mylib = ctypes.cdll.LoadLibrary('build/libmylib.so')
mylib._Z9say_hellov()

要么在 C++ 源文件里使用 extern "C" 声明为 C 兼容函数(但是没法重载了):

1
2
3
extern "C" DLLEXPORT void say_hello() {
printf("Hello, world!\n");
}

个人推荐后面一种方案。

读取哪一个文件

这里我们硬编码了 build\\mylib.dll 等路径,导致无法跨平台。
可以让 Python 运行时动态判断当前是什么系统,读取不同的文件路径和扩展名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import ctypes
import sys
import os

def load_library(path, name):
if sys.platform == 'win32': # *.dll
return ctypes.cdll.LoadLibrary(os.path.join(path, name + '.dll'))
elif sys.platform == 'linux': # lib*.so
return ctypes.cdll.LoadLibrary(os.path.join(path, 'lib' + name + '.so'))
elif sys.platform == 'darwin': # lib*.dylib
return ctypes.cdll.LoadLibrary(os.path.join(path, 'lib' + name + '.dylib'))
else:
raise ImportError('Unsupported platform: ' + sys.platform)

mylib = load_library('build', 'mylib')

这里我们用了 sys.platform 判断当前操作系统,os.path.join 在 Unix 类系统上是 '/'.join
Windows 上是 '\\'.joinpath 用作读取文件所在的目录,name 和 CMake 的目标名相同。

函数参数与返回

让我们定义几个带参数的函数作为测试:

1
2
3
4
5
6
7
8
9
10
11
extern "C" DLLEXPORT int twice_int(int x) {
return x * 2;
}

extern "C" DLLEXPORT float twice_float(float x) {
return x * 2.f;
}

extern "C" DLLEXPORT void print_str(const char *s) {
printf("str is: %s\n", s);
}

Python 调用的 C 函数可以有参数和返回值,不过 Python 实际并不知道有几个参数,分别是什么类型,
所以需要我们自己去指定参数和返回值的类型(如果不指定,默认参数全部为 int,返回值也为 int):

1
2
3
4
5
6
7
8
9
10
mylib.twice_int.argtypes = [ctypes.c_int]
mylib.twice_int.restype = ctypes.c_int
mylib.twice_float.argtypes = [ctypes.c_float]
mylib.twice_float.restype = ctypes.c_float
mylib.print_str.argtypes = [ctypes.c_char_p]
mylib.print_str.restype = None

print(mylib.twice_int(21)) # 42
print(mylib.twice_float(3.14)) # 6.28
mylib.print_str(b'Hello, C++!')
1
2
3
4
$ python main.py
42
6.28000020980835
str is: Hello, C++!

传递 NumPy 数组

有时候我们需要把一个 Python 端的数组传入/传出 C 语言的部分,可以传一个指针 + 一个大小来表示数组:

1
2
3
4
5
6
7
mylib.test_array.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
mylib.test_array.restype = None

import numpy as np

arr = np.random.rand(32).astype(np.float32)
mylib.test_array(arr.ctypes.data, arr.shape[0])
1
2
3
4
5
6
extern "C" DLLEXPORT void test_array(float *base, size_t size)
{
for (size_t i = 0; i < size; i++) {
printf("%ld: %f\n", i, base[i]);
}
}

注意到这里指针意味着按引用传递,因此也可以用于返回一个数组。

Read More

hexo deploy 时会删除 CNAME

在 GitHub 的 Settings -> Pages 页面设置了自定义域名 archibate.top
就可以实现 archibate.top 访问而不是 archibate.github.io 了。

然而发现每次 hexo d 都会导致定制域名失效。

原理:GitHub 所谓设置自定义域名,实质是在 gh-pages 分支添加了一个叫 CNAME
文本文件,内容为你的自定义域名。
hexo d 每次都会全部重写整个 gh-pages 的内容,也就是全新的 repo 去 force push
,导致 CNAME 文件被覆盖。

解决:原来 hexo 的 sources/ 文件夹内的内容都会直接出现在 gh-pages 里(下划线
开头的 _posts 除外)因此创建 sources/CNAME,内容为 archibate.top 即可在
下次 deploy 时候变成我的域名。

缺少 tags 和 categories 页面

可以看到在主页顶部有个叫 Tags 的链接,点击后跳转到 archibate.top/tags,出现 404 错误。

解决方法:

1
hexo new page tags

打开 sources/tags/index.md,修改内容为:

1
2
3
4
5
---
title: Tag Cloud
date: .......
layout: tags
---

categories 同理。

其中 layout: tags 表示使用 themes/freemind.386/layout/tags.ejs 作为渲染器(?)

hexo 设置多个部署目标

由于需要同时部署到 GitHub 和 Gitee,我希望 hexo d 能一次性部署两个。

解决:将

1
2
3
4
deploy:
type: git
repo: https://github.com/archibate/archibate.github.io.git
branch: gh-pages

修改成:

1
2
3
4
5
6
7
deploy:
- type: git
repo: https://github.com/archibate/archibate.github.io.git
branch: gh-pages
- type: git
repo: https://gitee.com/archibate/archibate.git
branch: gh-pages

其中 - 是 YAML 的列表表达方式,相当于 JSON 的 []

1
{deploy: [{type: "git", repo: "github"}, {type: "git", repo: "gitee"}]}

Git 也可以设置多个 push 目标

1
vim .git/config

将:

1
2
3
[remote "origin"]
url = https://github.com/archibate/archibate.github.io.git
fetch = +refs/heads/*:refs/remotes/origin/*

修改成:

1
2
3
4
[remote "origin"]
url = https://github.com/archibate/archibate.github.io.git
fetch = +refs/heads/*:refs/remotes/origin/*
url = https://gitee.com/archibate/archibate.git

这样以后 git push 会先推 GitHub,再推 Gitee,pull 也能同时拉两个。

就是有时候隔着代理推 Gitee 会出现 443 SSL_Error 的报错,以后研究
一下怎么绕过 domestic IP。或者把 Gitee 的 remote 改成 ssh 的。

Read More
post @ 2021-11-15

发现一个好看的 Hexo 主题:https://github.com/Haojen/hexo-theme-Claudia

根据 README 中的指引,安装:

1
2
3
npm install hexo-renderer-pug
npm install hexo-renderer-sass
npm install hexo-generator-search

出现错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
npm ERR! /home/bate/.node-gyp/16.11.1/include/node/v8-internal.h: In function ‘void v8::internal::PerformCastCheck(T*)’:
npm ERR! /home/bate/.node-gyp/16.11.1/include/node/v8-internal.h:492:38: error: ‘remove_cv_t’ is not a member of ‘std’; did you mean ‘remove_cv’?
npm ERR! 492 | !std::is_same<Data, std::remove_cv_t<T>>::value>::Perform(data);
npm ERR! | ^~~~~~~~~~~
npm ERR! | remove_cv
npm ERR! /home/bate/.node-gyp/16.11.1/include/node/v8-internal.h:492:38: error: ‘remove_cv_t’ is not a member of ‘std’; did you mean ‘remove_cv’?
npm ERR! 492 | !std::is_same<Data, std::remove_cv_t<T>>::value>::Perform(data);
npm ERR! | ^~~~~~~~~~~
npm ERR! | remove_cv
npm ERR! /home/bate/.node-gyp/16.11.1/include/node/v8-internal.h:492:50: error: template argument 2 is invalid
npm ERR! 492 | !std::is_same<Data, std::remove_cv_t<T>>::value>::Perform(data);
npm ERR! | ^
npm ERR! /home/bate/.node-gyp/16.11.1/include/node/v8-internal.h:492:63: error: ‘::Perform’ has not been declared
npm ERR! 492 | !std::is_same<Data, std::remove_cv_t<T>>::value>::Perform(data);
npm ERR! | ^~~~~~~
npm ERR! ../src/binding.cpp: In function ‘Nan::NAN_METHOD_RETURN_TYPE render(Nan::NAN_METHOD_ARGS_TYPE)’:

出错原因:std::remove_cv_t 是 C++14 引入的,而 gcc 默认为 C++11,感叹 C++ 拖后腿日常。

解决问题:

1
CXXFLAGS="--std=c++17" npm install hexo-renderer-sass
Read More

手把手教你在 C++17 中从零开始实现 ranges 库

关于为什么 C++20 引入了 ranges 库却没有 zip 还要用户自己实现这件事

So in this post, you will learn how to write a ranges library from scratch in C++17,
with some useful functionality that doesn’t exists in the C++20 library <ranges>,
like enumerate or zip.

C++11 range-based loop

It’s very convinent to use range-based for loop since C++11:

1
2
3
4
5
std::vector<int> list;

for (auto &&x: list) {
print(x + 1);
}

C++20 ranges library

But sometimes, I wonder what if I need to get the index while iterating over a range?
In Python, we can use enumerate, zip, map, and so on.
While there is no equivalant functions in C++, until C++20 which introduced many
range operations like std::views::transform for map in Python, use it like this:

1
2
3
4
5
6
7
8
9
#include <ranges>

std::vector<int> list;

for (auto &&x: list
| std::views::transform([] (auto &&x) { return x + 1; })
) {
print(x);
}

It will apply + 1 to every value in list before calling into the loop body.

Yeah, the operator| here - I’d like to call it the pipe style, is even more convinent than
Python’s function style range operations. Especially when the pipe is long:

1
2
3
4
5
6
7
8
9
10
11
#include <ranges>

std::vector<unique_ptr<int>> list;

for (auto &&x: list
| std::views::transform([] (auto &&x) { return x.get(); })
| std::views::transform([] (auto &&x) { return x + 1; })
| std::views::reverse
) {
print(x);
}

Of course, the function style also supported in C++20:

1
2
3
4
5
6
7
#include <ranges>

std::vector<int> list;

for (auto &&x: std::views::transform([] (auto &&x) { return x + 1; }, list)) {
print(x);
}

The lambda definition looks too verbose here, let’s simplify it by a macro:

1
2
3
4
5
6
7
8
9
#include <ranges>

#define LAMBDA1(x, ...) [&] (auto &&x) { return (__VA_ARGS__); })

std::vector<int> list;

for (auto &&x: list | std::views::transform(LAMBDA1(x, x + 1))) {
print(x);
}

Wait a miniute…

However, the transform or map isn’t so useful after all.
While they don’t even have zip and enumerate!!!
They are very commonly used in daily programming. Otherwise I still have to write for (int i... when the index is necessary.

So I decide to write my only ranges library rather than waiting for C++ standard to
support this.

My ranges library!

First of all, we need to define a generic range class.

1
2
3
4
5
6
7
8
9
10
11
12
template <class It>
struct range {
It m_begin;
It m_end;

constexpr range(It begin, It end)
: m_begin(std::move(begin)), m_end(std::move(end))
{}

constexpr It begin() const { return m_begin; }
constexpr It end() const { return m_end; }
};

Then, we we want to create a range for a std::vector<int>, we can use:

1
2
std::vector<int> list;
auto r = range<std::vector<int>::iterator>(list.begin(), list.end());

To be generic, we can use decltype to automatically determine the iterator type of a given object:

1
2
some_type_that_we_dont_know list;
auto r = range<decltype(list.begin())>(list.begin(), list.end());

Compile-time argument deduction

But this way we will have to write range<decltype(t.begin())>(t.begin(), t.end()) every time… Don’t worry!
We can use the complile-time argument deduction (CTAD) feature since C++17 to automatically deduce the
<class It> with given rule when argument has a confirmed type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <class It>
struct range {
It m_begin;
It m_end;

constexpr range(It begin, It end)
: m_begin(std::move(begin)), m_end(std::move(end))
{}

constexpr It begin() const { return m_begin; }
constexpr It end() const { return m_end; }
};

// define the deduction rule:
template <class It>
range(It begin, It end) -> range<It>;

Then, when range(t.begin(), t.end()) is called, it will be automatically deduced as range<decltype(t.begin())>(t.begin(), t.end()).

The pipable class

Given that most transformations only takes one range as input, thus can support the pipe style syntax (excluding zip which requires multiple input), we’d like to define a common class for all kind of range operators, called pipable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <class F>
struct pipable
{
F m_f;

constexpr pipable(F f)
: m_f(std::move(f))
{}

// function style: map(func)(range)
template <class ...Rs>
constexpr decltype(auto) operator()(Rs &&...rs) const {
return m_f(range(rs.begin(), rs.end())...);
}

// pipe style: range | map(func)
template <class R>
friend constexpr decltype(auto) operator|(R &&r, pipable const &self) {
return self(std::forward<decltype(r)>(r));
}
};

It is able to automatically convert f(arg) to pipe syntax arg | f.

Where the following function used the ... syntax since C++11:

1
2
3
4
template <class ...Rs>
constexpr decltype(auto) operator()(Rs &&...rs) const {
return m_f(range(rs.begin(), rs.end())...);
}

is equivalant to:

1
2
3
4
template <class R1, class R2, and_so_on>
constexpr decltype(auto) operator()(R1 &&r1, R2 &&r2, and_so_on) const {
return m_f(range(r1.begin(), r1.end()), range(r2.begin(), r2.end()), and_so_on);
}

with flexible number of arguments.

Implementing map

First let’s implement the easiest range operator: map

To do so, we need to implement our custom iterators, which requires many knowledge on what C++ iterators actually are.

range-based for loops are 纸老虎

For example, the following C++11 range-based for loop:

1
2
3
for (auto x: list) {
print(x);
}

Is nothing more than a shortcut for:

1
2
3
4
for (auto it = list.begin(); it != list.end(); ++it) {
auto x = *it;
print(x);
}

Where the begin and end are duck-typing method names for all objects to be considered iterable, so if you write:

1
2
3
4
int not_iterable = 42;
for (auto x: not_iterable) {
print(x);
}

The compiler will complain something like: error: int have no member named begin.
That’s because range-based for loops are just… nothing more than shortcuts…

Iterators are classes trying to imitate pointers

So what actually the type it is?

1
2
3
4
for (auto it = list.begin(); it != list.end(); ++it) {
auto x = *it;
print(x);
}

Looking at the *it here, we wonder if it is actually a pointer, i.e. int *?

Not really, the real type of list.begin() is vector<int>::iterator, which is a class with these operator overloading methods defined:

  • operator*() for getting the pointed value with *it
  • operator++() for iterate to the next value with ++it
  • operator!=() for comparing two iterators to see if it comes to end()

As you can see, C++ iterators are classes trying to behave as if it is a pointer, for allowing algorithm reuse ability, and minimize
the effort for C programmers to adapt from C pointers to C++ iterators.

So to implement custom iterators…

So if we want to implement our own iterators, we can simply define the above three operator overloading methods to control the behavior.

Now that we want to implement map, we acutally is willing the following code:

1
2
3
for (auto x: list | map(func)) {
print(x);
}

to be translated into:

1
2
3
4
for (auto it = list.begin(); it != list.end(); ++it) {
auto x = func(*it); // changed here
print(x);
}

right?

So why not just overload the operator*() to make it return func(*it) instead, while other operators remain the same.

Simple, just define a wrapper class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <class Func, class Base>
struct map_iterator {
Func m_func;
Base m_it;

// using `decltype(auto)` (since C++14) instead of `auto` so that
// even if `m_func` returns a reference, it automatically becames
// `auto &` rather than dereferencing that...
constexpr decltype(auto) operator*() const {
return m_func(*m_it);
}

constexpr map_iterator &operator++() {
++m_it;
return *this;
}

constexpr bool operator!=(map_iterator const &that) const {
return m_it != that.m_it;
}
};

template <class Func, class Base>
map_iterator(Func, Base) -> map_iterator<Func, Base>;

And the functor map that returns a range of map_iterator:

1
2
3
4
5
6
7
8
9
template <class F>
static constexpr auto map(F &&f) {
return pipable([=] (auto &&r) {
return range
( map_iterator{f, r.begin()}
, map_iterator{f, r.end()}
);
});
}

Here we used map_iterator{...} rather than map_iterator(...)
so that we don’t have to write the constructor ourselves but ask
the compiler to assign its members with default order (since C++11).

Then test it in main function:

1
2
3
4
5
6
7
int main() {
std::vector<int> list = {1, 2, 3, 4};
for (auto &&x: list | map([] (auto &&x) { return x + 1; })) {
std::cout << x << std::endl;
}
return 0;
}

Compile and run it:

1
2
3
4
5
$ g++ -std=c++20 a.cpp && ./a.out
2
3
4
5

Succeed! We managed to make our first step in implementing ranges library ourselves!

What’s next?

Next, we will go ahead to implement the enumerate and zip as well with the same
technique we learnt from implementing the map.

Implement enumerate

Now that we want to implement enumerate, we acutally is willing the following code:

1
2
3
for (auto [x, y]: list | enumerate(func)) {
print(x, y);
}

to be translated into:

1
2
3
4
5
size_t index = 0;
for (auto it = list.begin(); it != list.end(); ++it, ++index) {
auto [x, y] = std::pair(index, *it);
print(x, y);
}

right?

Here, the auto [x, y] = ... is the structual binding syntax since C++17,
and ... can be a std::pair, or std::tuple, or any thing unpackable, we will
take a deep look into this later.

So now we need to overload operator*() to make it return a pair of values, whose
first value is the index, which is incresed during operator++(), like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <class Base>
struct enumerate_iterator {
Base m_it;
std::size_t m_index = 0;

constexpr decltype(auto) operator*() const {
return std::pair<std::size_t, decltype(*m_it)>(m_index, *m_it);
}

constexpr enumerate_iterator &operator++() {
++m_it;
++m_index;
return *this;
}

constexpr bool operator!=(enumerate_iterator const &that) const {
return m_it != that.m_it;
}
};

template <class Base>
enumerate_iterator(Base) -> enumerate_iterator<Base>;

And since the enumerate takes no Func as input like map does, we can
simply define enumerate as a global variable:

1
2
3
4
5
6
static constexpr auto enumerate = pipable([] (auto &&r) {
return range
( enumerate_iterator{r.begin()}
, enumerate_iterator{r.end()}
);
});

The static here make sure that the symbol doesn’t conflict when the header
is being included for multiple times in a single project.

Test it again:

1
2
3
4
5
6
7
int main() {
std::vector<int> list = {1, 2, 3, 4};
for (auto &&[x, y]: list | enumerate) {
std::cout << x << ' ' << y << std::endl;
}
return 0;
}

Or use the function style if you like:

1
2
3
4
5
6
7
int main() {
std::vector<int> list = {1, 2, 3, 4};
for (auto &&[x, y]: enumerate(list)) {
std::cout << x << ' ' << y << std::endl;
}
return 0;
}

It should outputs:

1
2
3
4
0 1
1 2
2 3
3 4

Worked! Congrats on being Pythonic in modern C++ programming :)
Go ranges and no more for (int i = 0; ... bolierplates!

What about zip?

Homework time! Please try out what you’ve learnt by implement the zip
yourself, to test and solidify your new skill :)

For example:

1
2
3
4
5
6
7
8
int main() {
std::vector<int> list1 = {1, 2, 3, 4};
std::vector<int> list2 = {3, 3, 9, 9};
for (auto &&[x, y]: zip(list1, list2)) {
std::cout << x << ' ' << y << std::endl;
}
return 0;
}

should outputs:

1
2
3
4
1 3
2 3
3 9
4 9

I also prepared some challenge for curious readers, here they goes:

challenge A

  • Make zip resulting range size to be the minimum of all input ranges.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
std::vector<int> list1 = {1, 2, 3, 4, 5, 6, 7};
std::vector<int> list2 = {3, 9};
for (auto &&[x, y]: zip(list1, list2)) {
std::cout << x << ' ' << y << std::endl;
}
std::cout << "===" << std::endl;
for (auto &&[x, y]: zip(list2, list1)) { // order reversed!
std::cout << x << ' ' << y << std::endl;
}
return 0;
}

should outputs:

1
2
3
4
5
1 3
2 9
===
3 1
9 2

challenge B

  • Make zip takes n number of ranges as input, while operator*()
    returns a std::tuple with size n.

For example:

1
2
3
4
5
6
7
8
9
int main() {
std::vector<int> list1 = {1, 2, 3, 4};
std::vector<int> list2 = {3, 3, 9, 9};
std::vector<float> list3 = {3.14f, 2.718f, 1.414f, 0.618f};
for (auto &&[x, y, z]: zip(list1, list2, list3)) {
std::cout << x << ' ' << y << std::endl;
}
return 0;
}

should outputs:

1
2
3
4
1 3 3.14
2 3 2.718
3 9 1.414
4 9 0.618

Yes, the Python zip can pass above two ‘challenge’ :)

Finally…

You may ‘submit’ the ‘homework’ via 评论区, or GitHub, or any
other way that fesible. Submitting this homework won’t affect
anyone’s GPA or KPI, but this one is just for fun!

Below is the final source code of this tutorial, your homework
may be either based on it or completely from scratch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// compile with -std=c++17
#include <vector>
#include <iostream>


template <class It>
struct range {
It m_begin;
It m_end;

constexpr range(It begin, It end)
: m_begin(std::move(begin)), m_end(std::move(end))
{}

constexpr It begin() const { return m_begin; }
constexpr It end() const { return m_end; }
};

template <class It>
range(It, It) -> range<It>;


template <class F>
struct pipable
{
F m_f;

constexpr pipable(F f)
: m_f(std::move(f))
{}

template <class ...Rs>
constexpr decltype(auto) operator()(Rs &&...rs) const {
return m_f(range(rs.begin(), rs.end())...);
}

template <class R>
friend constexpr decltype(auto) operator|(R &&r, pipable const &self) {
return self(std::forward<decltype(r)>(r));
}
};


template <class Func, class Base>
struct map_iterator {
Func m_func;
Base m_it;

constexpr decltype(auto) operator*() const {
return m_func(*m_it);
}

constexpr map_iterator &operator++() {
++m_it;
return *this;
}

constexpr bool operator!=(map_iterator const &that) const {
return m_it != that.m_it;
}
};

template <class Func, class Base>
map_iterator(Func, Base) -> map_iterator<Func, Base>;

template <class F>
static constexpr auto map(F &&f) {
return pipable([=] (auto &&r) {
return range
( map_iterator{f, r.begin()}
, map_iterator{f, r.end()}
);
});
}


template <class Base>
struct enumerate_iterator {
Base m_it;
std::size_t m_index = 0;

constexpr decltype(auto) operator*() const {
return std::pair<std::size_t, decltype(*m_it)>(m_index, *m_it);
}

constexpr enumerate_iterator &operator++() {
++m_it;
++m_index;
return *this;
}

constexpr bool operator!=(enumerate_iterator const &that) const {
return m_it != that.m_it;
}
};

template <class Base>
enumerate_iterator(Base) -> enumerate_iterator<Base>;

static constexpr auto enumerate = pipable([] (auto &&r) {
return range
( enumerate_iterator{r.begin()}
, enumerate_iterator{r.end()}
);
});


int main() {
std::vector<int> list = {1, 2, 3, 4};
for (auto &&x: list | map([] (auto &&x) { return x + 1; })) {
std::cout << x << std::endl;
}
for (auto &&[x, y]: enumerate(list)) {
std::cout << x << ' ' << y << std::endl;
}
// TODO: implement zip...
return 0;
}

Also checkout my personal website [国内镜像站]
where this post were uploaded, Zhihu will also be uploaded synchronously.

Read More
post @ 2021-11-15

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

Read More
⬆︎TOP