百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

现代C++函数式编程 c++数学函数

liebian365 2024-10-22 15:40 17 浏览 0 评论



导读: 本文作者从介绍函数式编程的概念入手,分析了函数式编程的表现形式和特性,最终通过现代C++的新特性以及一些模板云技巧实现了一个非常灵活的pipeline,展示了现代C++实现函数式编程的方法和技巧,同时也体现了现代C++的强大威力和无限可能。


1

概述


函数式编程是一种编程范式,它有下面的一些特征:


  • 函数是一等公民,可以像数据一样传来传去
  • 高阶函数
  • 递归
  • pipeline
  • 惰性求值
  • 柯里化
  • 偏应用函数


C++98/03中的函数对象,和C++11中的Lambda表达式、std::function和std::bind让C++的函数式编程变得容易。我们可以利用C++11/14里的新特性来实现高阶函数、链式调用、惰性求值和柯理化等函数式编程特性。本文将通过一些典型示例来讲解如何使用现代C++来实现函数式编程。


2

高阶函数和pipeline的表现形式


高阶函数就是参数为函数或返回值为函数的函数,经典的高阶函数就是map、filter、fold和compose函数,比如Scala中高阶函数:


  • map


numbers.map((i: Int) => i * 2)


对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。


  • filter


numbers.filter((i: Int) => i % 2 == 0)


移除任何对传入函数计算结果为false的元素。


  • fold


numbers.fold(0) { (z, i) =>

a + i

}


将一个初始值和一个二元函数的结果累加起来。


  • compose


val fComposeG = f _ compose g _

fComposeG("x")


组合其它函数形成一个新函数f(g(x))。


上面的例子中,有的是参数为函数,有的是参数和返回值都是函数。高阶函数不仅语义上更加抽象泛化,还能实现“函数是一等公民”,将函数像data一样传来传去或者组合,非常灵活。其中,compose还可以实现惰性求值,compose的返回结果是一个函数,我们可以保存起来,在后面需要的时候调用。


pipeline把一组函数放到一个数组或是列表中,然后把数据传给这个列表。数据就像一个链条一样顺序地被各个函数所操作,最终得到我们想要的结果。它的设计哲学就是让每个功能就做一件事,并把这件事做到极致,软件或程序的拼装会变得更为简单和直观。

Scala中的链式调用是这样的:


s(x) = (1 to x) |> filter (x => x % 2 == 0) |> map (x => x * 2)


用法和Unix Shell的管道操作比较像,|前面的数据或函数作为|后面函数的输入,顺序执行直到最后一个函数。


这种管道方式的函数调用让逻辑看起来更加清晰明了,也非常灵活,允许你将多个高阶函数自由组合成一个链条,同时还可以保存起来实现惰性求值。现代C++实现这种pipeline也是比较容易的,下面来讲解如何充分借助C++11/14的新特性来实现这些高阶函数和pipeline。


3

实现pipeline的关键技术


根据前面介绍的pipeline表现形式,可以把pipeline分解为几部分:高阶函数,惰性求值,运算符|、柯里化和pipeline,把这几部分实现之后就可以组成一个完整的pipeline了。下面来分别介绍它们的实现技术。


?高阶函数


函数式编程的核心就是函数,它是一等公民,最灵活的函数就是高阶函数,现代C++的算法中已经有很多高阶函数了,比如for_each, transform:


std::vector<int> vec{1,2,3,4,5,6,7,8,9}

//接受一个打印的Lambda表达式

std::for_each(vec.begin(), vec.end(), [](auto i){ std::cout<<i<<std::endl; });

//接受一个转换的Lambda表达式

transform(vec.begin(), vec.end(), vec.begin(), [](int i){ return i*i; });


这些高阶函数不仅可以接受Lambda表达式,还能接受std::function、函数对象、普通的全局函数,很灵活。需要注意的是,普通的全局函数在pipeline时存在局限性,因为它不像函数对象一样可以保存起来延迟调用,所以我们需要一个方法将普通的函数转换为函数对象。std::bind也可以将函数转化为函数对象,但是bind不够通用,使用的时候它只能绑定有限的参数,如果函数本身就是可变参数的就无法bind了,所以,这个函数对象必须是泛化的,类似于这样:


class universal_functor

{

public:

template <typename... Args> auto operator()(Args&&... args) const ->decltype(globle_func(std::forward<Args>(args)...))

{

return globle_func(std::forward<Args>(args)...);

}

};


上面的函数对象内部包装了一个普通函数的调用,当函数调用的时候实际上会调用普通函数globle_func,但是这个代码不通用,它无法用于其他的函数。为了让这个转换变得通用,我们可以借助一个宏来实现function到functor的转换。


#define define_functor_type(func_name) class tfn_##func_name {\

public: template <typename... Args> auto operator()(Args&&... args) const ->decltype(func_name(std::forward<Args>(args)...))\

{ return func_name(std::forward<Args>(args)...); } }


//test code

int add(int a, int b)

{

return a + b;

}

int add_one(int a)

{

return 1 + a;

}


define_functor_type(add);

define_functor_type(add_one);


int main()

{

tnf_add add_functor;

add_functor(1, 2); //result is 3


tfn_add_one add_one_functor;

add_one_functor(1); //result is 2


return 0;

}


我们先定义了一个宏,这个宏根据参数来生成一个可变参数的函数对象,这个函数对象的类型名为tfn_加普通函数的函数名,之所以要加一个前缀tfn_,是为了避免类型名和函数名重名。define_functor_type宏只是定义了一个函数对象的类型,用起来略感不便,还可以再简化一下,让使用更方便。我们可以再定义一个宏来生成转换后的函数对象:


#define make_globle_functor(NAME, F) const auto NAME = define_functor_type(F);

//test code

make_globle_functor(fn_add, add);

make_globle_functor(fn_add_one, add_one);


int main()

{

fn_add(1, 2);

fn_add_one(1);


return 0;

}


make_globle_functor生成了一个可以直接使用的全局函数对象,使用起来更方便了。用这个方法就可以将普通函数转成pipeline中的函数对象了。接下来我们来探讨实现惰性求值的关键技术。


?惰性求值


惰性求值是将求值运算延迟到需要值时候进行,通常的做法是将函数或函数的参数保存起来,在需要的时候才调用函数或者将保存的参数传入函数实现调用。现代C++里已经提供可以保存起来的函数对象和lambda表达式,因此需要解决的问题是如何将参数保存起来,然后在需要的时候传给函数实现调用。我们可以借助std::tuple、type_traits和可变模版参数来实现目标。


template<typename F, size_t... I, typename ... Args>

inline auto tuple_apply_impl(const F& f, const std::index_sequence<I...>&, const std::tuple<Args...>& tp)

{

return f(std::get<I>(tp)...);

}


template<typename F, typename ... Args>

inline auto tuple_apply(const F& f, const std::tuple<Args...>& tp) -> decltype(f(std::declval<Args>()...))

{

return tuple_apply_impl(f, std::make_index_sequence<sizeof... (Args)>{}, tp);

}


int main()

{

//test code

auto f = [](int x, int y, int z) { return x + y - z; };

//将函数调用需要的参数保存到tuple中

auto params = make_tuple(1, 2, 3);


//将保存的参数传给函数f,实现函数调用

auto result = tuple_apply(f, params); //result is 0


return 0;

}


上面的测试代码中,我们先把参数保存到一个tuple中,然后在需要的时候将参数和函数f传入tuple_apply,最终实现了f函数的调用。tuple_apply实现了一个“魔法”将tuple变成了函数的参数,来看看这个“魔法”具体是怎么实现的。


tuple_apply_impl实现的关键是在于可变模版参数的展开,可变模版参数的展开又借助了std::index_sequence


?运算符operator|


pipeline的一个主要表现形式是通过运算符|来将data和函数分隔开或者将函数和函数组成一个链条,比如像下面的unix shell命令:


ps auwwx | awk '{print $2}' | sort -n | xargs echo


C++实现类似的调用可以通过重载运算符来实现,下面是data和函数通过|连接的实现代码:


template<typename T, class F>

auto operator|(T&& param, const F& f) -> decltype(f(std::forward<T>(param)))

{

return f(std::forward<T>(param));

}

//test code

auto add_one = [](auto a) { return 1 + a; };

auto result = 2 | add_one; //result is 3


除了data和函数通过|连接之外,还需要实现函数和函数通过|连接,我们通过可变参数来实现:


template<typename... FNs, typename F>

inline auto operator|(fn_chain<FNs...> && chain, F&& f)

{

return chain.add(std::forward<F>(f));

}


//test code

auto chain = fn_chain<>() | (filter >> [](auto i) { return i % 2 == 0; }) | ucount | uprint;


其中fn_chain是一个可以接受任意个函数的函数对象,它的实现将在后面介绍。通过|运算符重载我们可以实现类似于unix shell的pipeline表现形式。


?柯里化


函数式编程中比较灵活的一个地方就是柯里化(currying),柯里化是把多个参数的函数变换成单参数的函数,并返回一个新函数,这个新函数处理剩下的参数。以Scala的柯里化为例:


  • 未柯里化的函数


def add(x:Int, y:Int) = x + y


add(1, 2) // 3

add(7, 3) // 10


  • 柯里化之后


def add(x:Int) = (y:Int) => x + y


add(1)(2) // 3

add(7)(3) // 10


currying之后add(1)(2)等价于add(1,2),这种currying默认是从左到右的,如果希望从右到左呢,然而大部分编程语言没有实现更灵活的curring。C++11里面的std::bind可以实现currying,但要实现向左或向右灵活的currying比较困难,可以借助tuple和前面介绍的tuple_apply来实现一个更灵活的currying函数对象。


template<typename F, typename Before = std::tuple<>, typename After = std::tuple<>>

class curry_functor {

private:

F f_; ///< main functor

Before before_; ///< curryed arguments

After after_; ///< curryed arguments


public:


curry_functor(F && f) : f_(std::forward<F>(f)), before_(std::tuple<>()), after_(std::tuple<>()) {}

curry_functor(const F & f, const Before & before, const After & after) : f_(f), before_(before), after_(after) {}


template <typename... Args>

auto operator()(Args... args) const -> decltype(tuple_apply(f_, std::tuple_cat(before_, make_tuple(args...), after_)))

{

// execute via tuple

return tuple_apply(f_, std::tuple_cat(before_, make_tuple(std::forward<Args>(args)...), after_));

}


// currying from left to right

template <typename T>

auto curry_before(T && param) const

{

using RealBefore = decltype(std::tuple_cat(before_, std::make_tuple(param)));

return curry_functor<F, RealBefore, After>(f_, std::tuple_cat(before_, std::make_tuple(std::forward<T>(param))), after_);

}

// currying from righ to left

template <typename T>

auto curry_after(T && param) const

{

using RealAfter = decltype(std::tuple_cat(after_, std::make_tuple(param)));

return curry_functor<F, Before, RealAfter>(f_, before_, std::tuple_cat(after_, std::make_tuple(std::forward<T>(param))));

}

};


template <typename F>

auto fn_to_curry_functor(F && f)

{

return curry_functor<F>(std::forward<F>(f));

}


//test code

void test_count()

{

auto f = [](int x, int y, int z) { return x + y - z; };

auto fn = fn_to_curry_functor(f);


auto result = fn.curry_before(1)(2, 3); //0

result = fn.curry_before(1).curry_before(2)(3); //0

result = fn.curry_before(1).curry_before(2).curry_before(3)(); //0


result = fn.curry_before(1).curry_after(2).curry_before(3)(); //2

result = fn.curry_after(1).curry_after(2).curry_before(2)(); //1

}


从测试代码中可以看到这个currying函数对象,既可以从左边currying又可以从右边currying,非常灵活。不过使用上还不太方便,没有fn(1)(2)(3)这样方便,我们可以通过运算符重载来简化书写,由于C++标准中不允许重载全局的operater()符,并且operater()符无法区分到底是从左边还是从右边currying,所以我们选择重载<<和>>操作符来分别表示从左至右currying和从右至左currying。


// currying from left to right

template<typename UF, typename Arg>

auto operator<<(const UF & f, Arg && arg) -> decltype(f.template curry_before<Arg>(std::forward<Arg>(arg)))

{

return f.template curry_before<Arg>(std::forward<Arg>(arg));

}


// currying from right to left

template<typename UF, typename Arg>

auto operator>>(const UF & f, Arg && arg) -> decltype(f.template curry_after<Arg>(std::forward<Arg>(arg)))

{

return f.template curry_after<Arg>(std::forward<Arg>(arg));

}


有了这两个重载运算符,测试代码可以写得更简洁了。


void test_currying()

{

auto f = [](int x, int y, int z) { return x + y - z; };

auto fn = fn_to_curry_functor(f);


auto result = (fn << 1)(2, 3); //0

result = (fn << 1 << 2)(3); //0

result = (fn << 1 << 2 << 3)(); //0


result = (fn << 1 >> 2 << 3)(); //2

result = (fn >> 1 >> 2 << 3)(); //1

}


curry_functor利用了tuple的特性,内部有两个空的tuple,一个用来保存left currying的参数,一个用来保存right currying的参数,不断地currying时,通过tuple_cat把新currying的参数保存到tuple中,最后调用的时候将tuple成员和参数组成一个最终的tuple,然后通过tuple_apply实现调用。有了前面这些基础设施之后我们实现pipeline也是水到渠成。


?pipeline


通过运算符|重载,我们可以实现一个简单的pipeline:


template<typename T, class F>

auto operator|(T&& param, const F& f) -> decltype(f(std::forward<T>(param)))

{

return f(std::forward<T>(param));

}

//test code

void test_pipe()

{

auto f1 = [](int x) { return x + 3; };

auto f2 = [](int x) { return x * 2; };

auto f3 = [](int x) { return (double)x / 2.0; };

auto f4 = [](double x) { std::stringstream ss; ss << x; return ss.str(); };

auto f5 = [](string s) { return "Result: " + s; };

auto result = 2|f1|f2|f3|f4|f5; //Result: 5

}


这个简单的pipeline虽然可以实现管道方式的链式计算,但是它只是将data和函数通过|连接起来了,还没有实现函数和函数的连接,并且是立即计算的,没有实现延迟计算。因此我们还需要实现通过|连接函数,从而实现灵活的pipeline。我们可以通过一个function chain来接受任意个函数并组成一个函数链。利用可变模版参数、tuple和type_traits就可以实现了。


template <typename... FNs>

class fn_chain {

private:

const std::tuple<FNs...> functions_;

const static size_t TUPLE_SIZE = sizeof...(FNs);


template<typename Arg, std::size_t I>

auto call_impl(Arg&& arg, const std::index_sequence<I>&) const ->decltype(std::get<I>(functions_)(std::forward<Arg>(arg)))

{

return std::get<I>(functions_)(std::forward<Arg>(arg));

}


template<typename Arg, std::size_t I, std::size_t... Is>

auto call_impl(Arg&& arg, const std::index_sequence<I, Is...>&) const ->decltype(call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{}))

{

return call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{});

}


template<typename Arg>

auto call(Arg&& arg) const-> decltype(call_impl(std::forward<Arg>(arg), std::make_index_sequence<sizeof...(FNs)>{}))

{

return call_impl(std::forward<Arg>(arg), std::make_index_sequence<sizeof...(FNs)>{});

}


public:

fn_chain() : functions_(std::tuple<>()) {}

fn_chain(std::tuple<FNs...> functions) : functions_(functions) {}


// add function into chain

template< typename F >

inline auto add(const F& f) const

{

return fn_chain<FNs..., F>(std::tuple_cat(functions_, std::make_tuple(f)));

}


// call whole functional chain

template <typename Arg>

inline auto operator()(Arg&& arg) const -> decltype(call(std::forward<Arg>(arg)))

{

return call(std::forward<Arg>(arg));

}

};


// pipe function into functional chain via | operator

template<typename... FNs, typename F>

inline auto operator|(fn_chain<FNs...> && chain, F&& f)

{

return chain.add(std::forward<F>(f));

}


#define tfn_chain fn_chain<>()


//test code

void test_pipe()

{

auto f1 = [](int x) { return x + 3; };

auto f2 = [](int x) { return x * 2; };

auto f3 = [](int x) { return (double)x / 2.0; };

auto f4 = [](double x) { std::stringstream ss; ss << x; return ss.str(); };

auto f5 = [](string s) { return "Result: " + s; };

auto compose_fn = tfn_chain|f1|f2|f3|f4|f5; //compose a chain

compose_fn(2); // Result: 5

}


测试代码中用一个fn_chain和运算符|将所有的函数组合成了一个函数链,在需要的时候调用,从而实现了惰性求值。


fn_chain的实现思路是这样的:内部有一个std::tuple


template<typename Arg, std::size_t I>

auto call_impl(Arg&& arg, const std::index_sequence<I>&) const ->decltype(std::get<I>(functions_)(std::forward<Arg>(arg)))

{

return std::get<I>(functions_)(std::forward<Arg>(arg));

}


template<typename Arg, std::size_t I, std::size_t... Is>

auto call_impl(Arg&& arg, const std::index_sequence<I, Is...>&) const ->decltype(call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{}))

{

return call_impl(std::get<I>(functions_)(std::forward<Arg>(arg)), std::index_sequence<Is...>{});

}


在调用call_impl的过程中,将std::index_sequence不断展开,先从tuple中获取第I个function,然后调用获得第I个function的执行结果,将这个执行结果作为下次调用的参数,不断地递归调用,直到最后一个函数完成调用为止,返回最终的链式调用的结果。


至此我们实现具备惰性求值、高阶函数和currying特性的完整的pipeline,有了这个pipeline,我们可以实现经典的流式计算和AOP,接下来我们来看看如何利用pipeline来实现流式的mapreduce和灵活的AOP。


4

实现一个pipeline形式的mapreduce和AOP


前面的pipeline已经可以实现链式调用了,要实现pipeline形式的mapreduce关键就是实现map、filter和reduce等高阶函数。下面是它们的具体实现:


// MAP

template <typename T, typename... TArgs, template <typename...>class C, typename F>

auto fn_map(const C<T, TArgs...>& container, const F& f) -> C<decltype(f(std::declval<T>()))>

{

using resultType = decltype(f(std::declval<T>()));

C<resultType> result;

for (const auto& item : container)

result.push_back(f(item));

return result;

}


// REDUCE (FOLD)

template <typename TResult, typename T, typename... TArgs, template <typename...>class C, typename F>

TResult fn_reduce(const C<T, TArgs...>& container, const TResult& startValue, const F& f)

{

TResult result = startValue;

for (const auto& item : container)

result = f(result, item);

return result;

}


// FILTER

template <typename T, typename... TArgs, template <typename...>class C, typename F>

C<T, TArgs...> fn_filter(const C<T, TArgs...>& container, const F& f)

{

C<T, TArgs...> result;

for (const auto& item : container)

if (f(item))

result.push_back(item);

return result;

}


这些高阶函数还需要转换成支持currying的functor,前面我们已经定义了一个普通的函数对象转换为柯里化的函数对象的方法:


template <typename F>

auto fn_to_curry_functor(F && f)

{

return curry_functor<F>(std::forward<F>(f));

}


通过下面这个宏让currying functor用起来更简洁:


#define make_globle_curry_functor(NAME, F) define_functor_type(F); const auto NAME = fn_to_curry_functor(tfn_##F());


make_globle_curry_functor(map, fn_map);

make_globle_curry_functor(reduce, fn_reduce);

make_globle_curry_functor(filter, fn_filter);


我们定义了map、reduce和filter支持柯里化的三个全局函数对象,接下来我们就可以把它们组成一个pipeline了。


void test_pipe()

{

//test map reduce

vector<string> slist = { "one", "two", "three" };


slist | (map >> [](auto s) { return s.size(); })

| (reduce >> 0 >> [](auto a, auto b) { return a + b; })

| [](auto a) { cout << a << endl; };


//test chain, lazy eval

auto chain = tfn_chain | (map >> [](auto s) { return s.size(); })

| (reduce >> 0 >> [](auto a, auto b) { return a + b; })

| ([](int a) { std::cout << a << std::endl; });


slist | chain;

}


上面的例子实现了pipeline的mapreduce,这个pipeline支持currying还可以任意组合,非常方便和灵活。


有了这个pipeline,实现灵活的AOP也是很容易的:


struct person

{

person get_person_by_id(int id)

{

this->id = id;

return *this;

}


int id;

std::string name;

};

void test_aop()

{

const person& p = { 20, "tom" };

auto func = std::bind(&person::get_person_by_id, &p, std::placeholders::_1);

auto aspect = tfn_chain | ([](int id) { cout << "before"; return id + 1; })

| func

| ([](const person& p) { cout << "after" << endl; });


aspect(1);

}


上面的测试例子中,核心逻辑是func函数,我们可以在func之前或之后插入切面逻辑,切面逻辑可以不断地加到链条前面或者后面,实现很巧妙,使用很常灵活。


5

总结


本文通过介绍函数式编程的概念入手,分析了函数式编程的表现形式和特性,最终通过现代C++的新特性和一些模版元技巧实现了一个非常灵活的pipeline,展示了现代C++实现函数式编程的方法和技巧,同时也提现了现代C++的强大威力和无限的可能性。文中完整的代码可以从我的GitHub(https://github.com/qicosmos/cosmos/blob/master/modern_functor.hpp)上查看。


本文的代码和思路参考和借鉴了http://vitiy.info/templates-as-first-class-citizens-in-cpp11/,在此表示感谢。

免费C++ 入门教程:点击下方了解更多

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: