Prelude

May, she will stay
Resting in my arms again
– April Come She Will, Simon & Garfunkel, Sounds of Silence

不知不觉五月都要过去了,而我所在的城市也迎来了真正的夏天。中午的气温已经攀到了35摄氏度,这种天气下躲在室内做点喜欢的事情是很不错的。

虽然引用了美国民歌两兄弟的歌曲,但这篇主要还是关于技术和折腾。

C++ range 库中 view 的实现

一直好奇 range 库中的 view 是怎么在不保存容器的前提下对容器进行操作的,当然很容易推测出可能存了容器的迭代器或者引用、指针等等,但还是不免对代码上的实现产生疑问。本来以为花 10 分钟看一下 view 的代码实现就能知道,因为开始走错了方向,竟然断断续续地研究了 2 天!

我们拿 take_view 的代码举例。创建一个 take_view 的方法有两种:

1
2
3
4
5
6
7
std::vector<int> vec{ 1, 2, 3, 4, 5 };
// 方法一:采用管道符
auto vec_take_3 = vec | std::views::take(3);
// 方法二:调用函数接口
auto vec_take_3 = std::views::take(vec, 3);
// 下面的方法也可以,但不推荐
auto vec_take_3 = std::ranges::take_view(vec, 3);

至于最后的方法为什么不推荐,可以参考 Barry 大佬的博客 prefer views::meow 里面讲得很清楚。

现在想知道的是,vec_take_3 这个对象是怎么保存 vec 的。直接上 range-v3 的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template<typename Rng>
struct take_view : view_interface<take_view<Rng>, finite>
{
private:
  Rng base = Rng(); // 这是内部保存的容器
  // 现在关键就在于 Rng 是什么类型的
  // 省略,直接看构造函数
  // ...
public:
  constexpr take_view(Rng base, range_difference_t<Rng> cnt)
    : base_(std::move(base)) ,count_(cnt) {}
};

乍一看这个实现有非常大的迷惑性,直接调用这个构造函数不是直接将 std::vector 复制了一遍(因为这是 copy 传值),然后再用 std::move 把它转移到内部的 base 成员上吗?而且这里的 Rng 推导出来也是 std::vector ,并不是我们预期的引用或指针。

因此,我就是在这上面卡了挺久。另外,我当时并没有直接看 range-v3 库的实现,而是看的微软官方更具有迷惑性的实现,这里不再详述。

原来破解之道不在类的定义里面,而在外面的简单两句:

1
2
3
template<typename Rng>
take_view(Rng &&, range_difference_t<Rng>)
  -> take_view<views::all_t<Rng>>;

这个叫 deduction guide ,是 C++17 里引入的特性,这两句的意思是无论来的是什么类型,编译器都应该把 take_view 的类型推导为 views::all_t<Rng> 类型。当传入的是 std::vector<int> 类型名时,这里的 Rng 被推导为 std::vector<int>& 类型。于是 take_view 里的 Rng 实际就是 all_t 类型,而这个 all_t 也是一个类型别名,它像选择器一样选择一个合适的 view 类型,定义如下:

1
2
template<typename Rng>
using all_t = decltype(all(std::declval<Rng>()));

all_t 类型是把 Rng 的实例传给仿函数 all 后的返回值决定的。而当 Rng 为 std::vector<int>& 时,all 中的调用:

1
2
3
4
5
6
7
8
9
template(typename T)(
    requires range<T &> AND viewable_range<T>)
constexpr auto operator()(T && t) const
{
    return all_fn::from_range_(static_cast<T &&>(t),
                               meta::bool_<view_<uncvref_t<T>>>{},
                               std::is_lvalue_reference<T>{},
                               meta::bool_<borrowed_range<T>>{});
}

from_range_ 中的参数被推导为 (t, false_type, true_type, true_type) ,于是编译器决定调用的重载形式为:

1
2
3
4
5
6
template<typename T>
static constexpr auto from_range_(T && t, std::false_type, std::true_type,
                                  detail::ignore_t)
{
    return ranges::views::ref(t);
}

最终,我们知道这里 all_t 是 ref_view<std::vector<int>> 类型的别名。因此, take_view 的构造函数就变成了:

1
2
constexpr take_view(ref_view<std::vector<int>> base, range_difference_t<Rng> cnt)
  : base_(std::move(base)) ,count_(cnt) {}

这里用了隐式的构造,调用 ref_view 的构造函数:

1
2
constexpr ref_view(Rng & rng) noexcept
  : rng_(detail::addressof(rng)) {}

其中,Rng 为 std::vector<int> ,很简单这是只是对 rng 引用的 vec 取了一个地址,再把地址保存在 ref_view 内部的指针 rng_ 上:

1
Rng * rng_ = nullptr;

到这里真相大白, 原来 take_view 内部的那个 base 在当前的编译器推导下就是一个 ref_view ,这也不难理解为什么 take_view 的构造函数用拷贝传值,然后再 move 到内部的成员上了。而这一切的误会都是因为我没看见 take_view 定义后面的 deduction guide 。之后还是要多加小心。

用 gitea 搭建自己的代码托管服务器

最近因为有一些 host 私有代码的需要,所以搭了个自己的代码托管服务器。选用的框架是开源的 gitea ,足够轻量级,而且搭建起来非常方便,个人和小型团队完全够用了,在此感谢 gitea 的作者。我喜欢它的名字和 Logo ,一杯茶。写代码就应该是这样的心态,手上端着一杯茶,细细品味和琢磨。直接参考官方文档花1个小时就能搭建完成,我用的是 MariaDB 直接放在服务器上与 gitea 一同运行。

想不到最后居然是栽在了 Windows 上,不,应该是 Visual Studio 。事情是我把 gitea 搭建好添加好 ssh key 准备往上 push/pull 代码的时候,在 windows 上死活出现 Permission Denied 错误,折腾了一会儿发现使用 Git Bash 就不会有这种错误,通过 ssh -vT git@ip 命令发现是 OpenSSH 版本的问题, Git 因为使用的是自带的 OpenSSH 所以没有出来这种问题,可以正常 push/pull 。

于是,我去 windows openssh 的仓库下载了最新的 win32 openssh 安装好后,在 powershell/cmd 里能正常 push/pull 代码了。到这里我以为问题已经解决了,但在 Visual Studio 里修改完代码准备 push 到 gitea 服务器的时候发现这里还是 push 不了,打开输出窗口一看仍然是之前的那个 Permission Denied 错误。一开始我怀疑是 Visual Studio 自带的 Git 版本有问题,进【工具-选项-项目和解决方案-Web包管理-外部Web工具】里找到它自带 Git 的目录,显示为 $(DevEnvDir)\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\Git\cmd 。去 Git 官网下载最新的 Portable 压缩包直接把 Git 文件夹整体替换掉了。这时,自带的 Git 就是最新版的。然后再次使用 Visual Studio 进行仓库的 push ,发现问题还是没解决。

再折腾了很久,想着是不是 Visual Studio 自带的 Git 用的不是系统的 OpenSSH 而是其他什么地方的 ssh 。于是再上网查询,通过直接在 .gitconfig 文件中指定 ssh 的位置让 Git 统一使用系统的 OpenSSH :

1
2
[core]
        sshCommand = '"C:\\Program Files\\OpenSSH\\ssh.exe"'

这里做了之后,终于问题得到了解决。