在本文中,writer用于将某个变量写入到硬盘上,而reader则从硬盘上读取该变量的值;不考虑完整的序列化/反序列化特性和跨平台兼容性。
reader/writer的此番操作虽然可以被比较容易地实现,但是写法各不相同。较为常见的有遵循STL库流式操作(input/output stream)的写法:
// write x to the disk
writer w("dump.bin");
int x = 1;
w << x;
// read x from the disk
reader r("dump.bin");
int x;
r >> x;
然而,这其中存在两个问题:
1)在上述的reader例子中,x
需要被提前定义,显得不够优雅;更进一步的,假如x
不能被默认初始化,或者初始化具有non-trivial的开销,就会造成书写或者性能上的问题;
2)这样的写法并不能很好地支持数组,特别是多维数组的存取。
本文介绍如下更为便捷的写法:
(写入单个变量或数组)
writer w("dump.bin");
// ------ write a single value ------
w('A');
double b = 1.0;
w(b);
// ------ write an array ------
std::vector v{
std::vector{48,49,50,51},
std::vector{97,98,99,100,101},
std::vector{65,66}
};
w(v); // v is a multidimensional array
w(std::vector<int>(2'000'000'000));
(读取单个变量或数组)
reader r("dump.bin");
// ------ read a single value ------
char a = r();
auto b = (double)r();
// auto x = r(); // error; read an auto type is not allowed
// ------ read an array ------
std::vector<std::vector<int>> v = r({4,5,2}); // an array with three dimensions
std::vector<int> v2 = r(11); // a single-dimensional array
注意到上述reader
的写法中,用户并不需要显式为r
的operator()
调用指定返回值类型;相反的是,它能自动根据接收变量的类型来决定。
我首先介绍这部分语法的实现,然后简单讲述对多维数组的支持。
reader调用不显式声明类型
不显式声明这件事,对于writer来说,相当的容易。对于一个writer,由于其是一个callable object,拥有小括号重载template<typename T> void operator(T x)
,我们可以轻易地得到x
的类型T
,并进行相应的写操作。但对于reader来说,对它的小括号调用是无参数的;如果声明成template<typename T> T operator()
并在模板中显式填写返回值类型,由于C++的语法限制,并不能写成auto x = r<int>()
,而是得写成auto x = r.operator()<int>()
,变得又长又丑。更好的写法是,对x
声明类型,而让r的返回值自动匹配,即int x = r()
我们让reader返回一个临时类型,称为transformer
,并为其重载强制类型转换符template<typename T> transformer::operator T()
。这样一来,int x = r()
事实上就等价为如下语句
transformer t = r();
int x = t; // [ T = int ]
如此一来,x
的类型信息便在transformer
的类型转换重载中被获取了。
接下来的问题是,transformer
既然是一个中间类型,我们并不想将它向用户公开;换而言之,我们要禁止用户通过auto t = r()
的方式获取到具体的transformer
对象。
一个简单的想法是,我们通过不公开其构造函数(声明为private/protected)来阻止用户构造;但这并不管用;考虑如下场景:
struct transformer{
template<typename T>
operator T() const{...}
private:
transformer(){}
transformer(const transformer&) = delete;
transformer(transformer&&) = delete;
friend class reader;
};
struct reader{
...
transformer operator(){
return transformer();
}
};
int main(){
auto t = reader()(); // Fail to report errors
return 0;
}
尽管看起来在main
函数的第二行,用户调用了已被删除的transformer
的移动构造函数,从而触发编译错误;但是,由于copy elision的缘故,auto t = reader()()
被直接解释为在reader::operator()
中对main::t
的初始化,而无需调用移动构造函数。
解决方法是让reader::operator()
返回一个引用transformer&
,从而规避copy elision,触发构造函数。虽然用户依然可以使用auto &t = reader()()
的写法来获得一个transformer
的引用,但是由于该写法本来就是非法的(正常情况下无法写作auto &t= r()
),故不对此额外处理。
在实际实现中,每个reader
对象在其内部存储一份transformer
变量,并在reader::operator()
中返回其引用。为了减小拷贝开销,该transformer
仅存储了其外部reader
对象的this
指针。transformer::operator T()
会获取到类型T
并转调用reader::read<T>
来进行实际的文件读取和变量初始化操作。示意代码如下:
class mmap_reader
{
class transformer_base{
transformer_base(mmap_reader *r) : r(r), arg(nullptr){}
transformer_base(const transformer_base&) = delete;
transformer_base(transformer_base&&) = delete;
transformer_base& operator=(const transformer_base&) = delete;
transformer_base& operator=(transformer_base&&) = delete;
friend mmap_reader;
protected:
mmap_reader *r;
const void *arg;
};
template<typename A=void>
class transformer : transformer_base{
friend mmap_reader;
public:
template<typename T>
operator T() const{
if constexpr(std::is_void_v<A>)
return this->r->read<T>();
else
return this->r->read<T>(*(const A*)(this->arg));
}
};
transformer_base gen = this;
...
public:
template<typename T>
[[nodiscard]] T read() requires(std::is_trivially_copyable_v<T>) {...}
template<std::ranges::range R, class U>
[[nodiscard]] R read(const U &sizes) {...}
template<std::ranges::range R, class U>
[[nodiscard]] R read(const std::initializer_list<U> &sizes)
{
return read<R>(std::ranges::ref_view(sizes));
}
[[nodiscard]] transformer<>& operator()() // RETURN THE REF
{
gen.arg = nullptr;
return static_cast<transformer<>&>(gen);
}
template<typename U>
[[nodiscard]] transformer<U>& operator()(const U &sizes)
{
gen.arg = &sizes;
return static_cast<transformer<U>&>(gen);
}
template<typename U>
[[nodiscard]] auto operator()(const std::initializer_list<U> &sizes)
-> transformer<std::initializer_list<U>>&
{
gen.arg = &sizes;
return static_cast<
transformer<std::initializer_list<U>>&
>(gen);
}
...
};
注意到为了支持对数组的读取,上述代码还增加了对reader::read
和reader::operator()
的重载,以及在transformer
中引入了额外的成员arg
来暂存数组长度信息。
对多维数组的支持
对多维数组支持的实现难点主要体现在writer
中,分别是1)如何获取传入数组的各维长度并计算各元素在文件中的偏移量;2)如何将元素并行写入buffer。出于篇幅考量,本文仅开第一部分;示意代码如下:
template<std::ranges::range R>
static auto calc_offset(const R &r) requires(
std::is_trivially_copyable_v<std::ranges::range_value_t<R>> &&
!std::ranges::range<std::ranges::range_value_t<R>>)
{
return std::pair(
util::delayed_seq(std::ranges::size(r)+1, [&](size_t i){
return i * sizeof(std::ranges::range_value_t<R>);
}),
0
);
}
template<std::ranges::range R>
static auto calc_offset(const R &r) requires
std::ranges::range<std::ranges::range_value_t<R>>
{
using cm = custom<typename lookup_custom_tag<R>::type>;
using value_t = std::ranges::range_value_t<R>;
using ret_t = decltype(calc_offset(std::declval<value_t>()));
auto sub = util::init<typename cm::seq<ret_t>>(
std::ranges::size(r),
[&](size_t i){return calc_offset(r[i]);}
);
auto sizes = util::delayed_seq(sub.size(), [&](size_t i){
return sub[i].first.back();
});
auto [offset,tot] = cm::scan(sizes);
offset.push_back(tot);
return std::pair(std::move(offset), std::move(sub));
}
我们逐维度递归并计算每一层元素的偏移量;递归的base case是对于当前层的元素类型R
,判断R
是否是满足is_trivially_copyable_v<range_value_t<R>> && !range<range_value_t<R>>
;即它的成员并非range
,意味着R
已经是多维数组的最后一层,而且是trivially copyable,数据结构可以被安全地写入到文件里。
在上述代码中,calc_offset
的第一个重载对应着base case,而第二个重载则是继续递归。之所以以重载的形式实现递归,是因为每一层递归的返回值类型都不相同,因此需要通过实例化不同的函数模板来实现。