CPPCON2020:C++20 Ranges in Practice
CPPCON2020:C++20 Ranges in Practice
本次演讲提供了三个问题讲解
- least element of an array
- sum of squares
- string trimming
least element of an array
该问题是找到container中的最小值。
之前(c++17)的代码是这样的。
std::vector<int> vec = get_input();
auto iter = std::min_element(vec.begin(),vec.end());
但是有了ranges,我们可以就可以这样写
auto iter = std::ranges::min_element(vec);
还是挺方便的,可读性也很高。
但这里还需要为了获得输入引入vec这一变量,假如我们之后不再使用vec,能不能把他省略掉?比如:
auto iter = std::ranges::min_element(get_input());
答案是不可以,因为get_input()产生一个右值,之后右值会销毁,iter变成了一个悬挂(dangling)的迭代器,指向一个被销毁的内存。
当你使用这个iter时(*iter),程序会报错。
你会发现auto的自动推导为std::ranges::dangling类型
所以当你算法传递一个右值时,ranges会返回一个dangling object。ranges通过实现了dangling protection,它会提醒你潜在的悬挂指针或悬挂迭代器的情况。
但是,有一些类型,他们的iterator可以安全的超出类型的生存周期。
比如string_view,span。
std::string str = "Helloworld";
auto iter = std::string_view{str}.begin();
std::cout<<*iter;
string_view生成的匿名对象在语句完成后立即销毁,但是iter却可以继续使用。所以这类类型我们不需要dangling protetion,我们可以显式的告诉编译器为这种类型取消dangling protection
template<>
inline constexpr bool
std::ranges::enable_borrowed_range<T> = true;
比如我们现在为vector自定义一个vector_view
template<typename T,typename alloc>
class vector_view : public std::ranges::view_interface<vector_view<T,alloc>>{
public:
vector_view() = default;
vector_view(const vector<T,alloc>& vec): m_begin(vec.cbegin()), m_end(vec.cend()){}
auto begin() {return m_begin;}
auto end() {return m_end;}
private:
vector<T,alloc>::const_iterator m_begin{},m_end{};
};
template<typename T, typename alloc>
vector_view<T,alloc> get_vector_view(vector<T,alloc>& vec){
return vector_view{vec};
}
int main(){
vector<int> test = {5,4,3,2,1};
auto iter = std::ranges::min_element(get_vector_view(test));
cout<<*iter;
return 0;
}
继承view_interface能够快速帮我们实现一个自定义view类,我们只需要管理好begin和setinel这两个iterator就行了。
如果你运行这个程序,你会发现*iter这里出现错误。
但是我们加上:
template<typename T, typename alloc>
inline constexpr bool
std::ranges::enable_borrowed_range<vector_view<T,alloc>> = true;
之后,程序就能正常输出1了。
……
ranges有个概念borrowed range,一个borrowed range为:
- 左值
- 一个使用
enable_borrowed_range<R> = true的右值。
views
接下来视频大概聊了一些views,对于views的相关内容,我更推荐读者去寻找cppreference或者其他更好的文章去了解。
视频中提到,就像刚才的例子,我们通过继承view_base或者view_interface来自定义一个view类。
以及range适配器只能使用那些viewable ranges。
以及举了个例子:
auto vec = get_vector();
auto v1 = vec | views::transform(func);
// ok,vec is an lvalue
auto v2 = get_span() | views::transform(func);
//ok,span is borrrowed(and usually a view)
auto v3 = subrange(vec.begin(),vec.end()) | views::transform(func);
//ok,subrange is borrowed(and a view)
auto v4 = get_vector() | views::transform(func);
//ERROR:get_vector() returns an rvalue vector, which is neither a view or a borrowed range
subrange是range lib中的,所以没问题。
还是我说的,这部分我更推荐自己找资料。
sum of squares
这是视频的第二个主题。
它的目的是返回一个数组的每一项的平方和。
老方法为:
auto vec = get_input();
std::transform(vec.begin(),vec.end(),vec.begin(),[](int i){return i*i;});
auto sum = std::accumulate(vec.begin(),vec.end(),0);
有了ranges,我们可以:
auto vec = get_input();
std::transform(vec,vec.begin(),[](int i){return i*i;});
auto sum = std::accumulate(vec.begin(),vec.end(),0);
但我们还可以在简单一点:
auto vec = get_input();
auto view = std::ranges::views::transform(vec,[](int i){return i*i;});
auto sum = std::accumulate(view.begin(),view.end(),0);
我们可以理解为view变量存储了一种运算方法,只有当我们真正地访问view中的元素时,transform的运算才会施加在元素上——这是一种lazy operation。
如果想知道它是如何作用的,读者可以自行尝试对view中的元素访问,举例来说:
int sum = 0;
for(int i:view){
sum += i;
}
sum的最终结果和上述accmulate方法一致。
我们还可以用上ranges中的pipe写法,即:
auto vec = get_input();
auto view = vec
| std::ranges::views::transform([](int i){return i*i;});
| std::ragnes::views::common;
auto sum = std::accumulate(vec.begin(),vec.end(),0);
类似链式调用,我们运算通过重载的|运算符将这些操作一步一步施加上去。因为accumulate要求两个迭代器的类型一致,所以我们使用common,当然如果view本身前后的迭代器就是一致的,去掉common也是可以的。
因为numeric还没有实现ranges,所以视频后面自己写了个ranges的accumulate.
#define _RANGE std::ranges::
namespace my {
//base version
template<input_iterator I, sentinel_for<I> S,
typename Init = iter_value_t<I>,
typename Op = std::plus<>,
typename Proj = identity>
Init accumulate(I first, S last, Init init = Init{}, Op op = Op{},Proj proj = Proj{}) {
while (first != last) {
init = std::invoke(op, std::move(init), std::invoke(proj, *first));
++first;
}
return init;
}
//overload version
template<_RANGE input_range R,
typename Init = _RANGE range_value_t<R>,
typename Op = std::plus<>,
typename Proj = identity>
Init accumulate(R&& rng, Init init = Init{}, Op op = Op{}, Proj proj = Proj{}) {
return accumulate(_RANGE begin(rng), _RANGE end(rng), std::move(init), std::move(op), std::move(proj));
}
}
然后,accumulate就可以这样使用
auto sum = my::accumulate(get_input(),{},{},[](int i){return i*i;});
或者传入直接range也行。
string trimming
这一部分作者主要是带领我们体会了一下利用operator | 进行函数式编程。没什么可讲得的。
主要目的是整理字符串,将一个字符串前后的white-space给去掉,比如字符串“\f\n\t\r\vHello\f\n\t\r\v ”经过处理后变成“Hello”
给出最后的代码
template<typename R>
auto trim_front(R&& rng) {
return forward<R>(rng)
| _RANGES views::drop_while(::isspace);
}
template<typename R>
auto trim_back(R&& rng) {
return forward<R>(rng)
| _RANGES views::reverse
| _RANGES views::drop_while(::isspace)
| _RANGES views::reverse;
}
template<typename R>
auto trim(R&& rng) {
return trim_back(trim_front(forward<R>(rng)));
}
std::string trim_str(const std::string& str) {
//ranges::to在C++20还没有实现,你需要自己实现或者用别人实现的,比如rangesv3
return trim(str) | std::ranges::to<std::string>;
}
有两个额外的地方值得一提。
比如trim_front,他也是一个lazy operation,我们可以仅仅返回drop_while本身,而不管它传进来的数据。
auto trim_front(){
return _RANGS views::drop_while(::isspace);
}
而我们调用的时候就是这样:
trim_front()(rng);
这样,上述所有的代码就可简化为:
auto trim_front() {
return _RANGES views::drop_while(::isspace);
}
auto trim_back() {
return _RANGES views::reverse
| _RANGES views::drop_while(::isspace)
| _RANGES views::reverse;
}
auto trim() {
return trim_front() | trim_back();
}
std::string trim_str(const std::string& str) {
//ranges::to在C++20还没有实现,你需要自己实现或者用别人实现的,比如rangesv3
return trim()(str) | std::ranges::to<std::string>;
}
再次简化,我们可以将上述函数变成一个对象
inline constexpr auto trim_front =
_RANGS views::drop_while(::isspace);
所以上述代码再次简化
inline constexpr auto trim_front = views::drop_while(::isspace);
inline constexpr auto trim_back = views::reverse | views::drop_while(::isspace) | views::reverse;
inline constexpr auto trim = trim_back | trim_front;
std::string trim_str(const std::string& str) {
return str | trim | std::ranges::to<std::string>;
}

浙公网安备 33010602011771号