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_viewspan

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>;
}
posted @ 2022-05-27 16:27  ᴮᴱˢᵀ  阅读(132)  评论(0)    收藏  举报