帮助您编写简洁易读的 C++ 代码。
优秀的代码大多应该是自我文档化的,但在现实中使用 C++ 时,您可能会发现自己正在处理低级的东西,例如迭代器或手写循环,这些东西会分散您对代码实际本质的注意力。
FunctionPlus 是一个小型的仅包含头文件的库,支持您减少代码噪音并一次仅处理一个抽象级别。从长远来看,通过提高代码的简洁性和可维护性,它可以提高生产力(和乐趣!)。它通过提供纯粹且易于使用的功能来实现这些目标,使您无需一遍又一遍地实现常用的控制流。
假设您有一个数字列表,并且只对奇数感兴趣。
bool is_odd_int ( int x) { return x % 2 != 0 ; }
int main ()
{
typedef vector< int > Ints;
Ints values = { 24 , 11 , 65 , 44 , 80 , 18 , 73 , 90 , 69 , 18 };
// todo: get odd numbers from values ...
}
实现您的目标有不同的可能性。其中一些是:
Ints odds;
for ( int x : values)
{
if ( is_odd_int (x))
{
odds. push_back (x);
}
}
std::copy_if
Ints odds;
std::copy_if (std::begin(values), std::end(values),
std::back_inserter(odds), is_odd_int);
FunctionalPlus
中的keep_if
auto odds = fplus::keep_if(is_odd_int, values);
如果您认为版本 3 可能是使用起来最愉快的版本,您可能会喜欢FunctionalPlus。如果您仍然认为手写的 for 循环更容易理解,还请考虑如果循环体(即调用fplus::keep_if
中的相应 lambda 函数)更长会发生什么。当阅读keep_if
时,您仍然会立即知道odds
只能包含来自values
并由某些(可能很复杂)谓词选择的元素。在 for 循环的情况下,在阅读整个循环体之前,您不知道发生了什么。循环版本可能需要在顶部添加注释,说明keep_if
的使用乍一看会告诉您什么。
下面是一些简短的示例,展示了您可以使用FunctionalPlus 对函数和容器做的好事。
您可以测试容器内容的各种属性,例如
# include < fplus/fplus.hpp >
# include < iostream >
int main ()
{
std::list things = { " same old " , " same old " };
if ( fplus::all_the_same (things))
std::cout << " All things being equal. " << std::endl;
}
team
中的我还有一些用于检索容器属性的便利函数。例如,您可以计算字符串中某个字符的出现次数。
# include < fplus/fplus.hpp >
# include < iostream >
int main ()
{
std::string team = " Our team is great. I love everybody I work with. " ;
std::cout << " There actually are this many 'I's in team: " <<
fplus::count ( " I " , fplus::split_words ( false , team)) << std::endl;
}
输出:
There actually are this many 'I's in team: 2
与手写版本相比,在容器中查找评分最高的元素非常简单(1, 2)。
# include < fplus/fplus.hpp >
# include < iostream >
struct cat
{
double cuteness () const
{
return softness_ * temperature_ * roundness_ * fur_amount_ - size_;
}
std::string name_;
double softness_;
double temperature_;
double size_;
double roundness_;
double fur_amount_;
};
void main ()
{
std::vector cats = {
{ " Tigger " , 5 , 5 , 5 , 5 , 5 },
{ " Simba " , 2 , 9 , 9 , 2 , 7 },
{ " Muffin " , 9 , 4 , 2 , 8 , 6 },
{ " Garfield " , 6 , 5 , 7 , 9 , 5 }};
auto cutest_cat = fplus::maximum_on ( std::mem_fn (&cat::cuteness), cats);
std::cout << cutest_cat. name_ <<
" is happy and sleepy. *purr* *purr* *purr* " << std::endl;
}
输出:
Muffin is happy and sleepy. *purr* *purr* *purr*
假设您给出了以下函数。
std::list< int > collatz_seq ( int x);
并且您想要创建一个std::map
其中包含 30 以下所有数字的 Collatz 序列的字符串表示形式。您也可以通过函数方式很好地实现这一点。
# include < fplus/fplus.hpp >
# include < iostream >
// std::list collatz_seq(std::uint64_t x) { ... }
int main ()
{
typedef std::list< int > Ints;
// [1, 2, 3 ... 29]
auto xs = fplus::numbers( 1 , 30 );
// A function that does [1, 2, 3, 4, 5] -> "[1 => 2 => 3 => 4 => 5]"
auto show_ints = fplus::bind_1st_of_2 (fplus::show_cont_with, " => " );
// A composed function that calculates a Collatz sequence and shows it.
auto show_collats_seq = fplus::compose (collatz_seq, show_ints);
// Associate the numbers with the string representation of their sequences.
auto collatz_dict = fplus::create_map_with (show_collats_seq, xs);
// Print some of the sequences.
std::cout << collatz_dict[ 13 ] << std::endl;
std::cout << collatz_dict[ 17 ] << std::endl;
}
输出:
[13 => 40 => 20 => 10 => 5 => 16 => 8 => 4 => 2 => 1]
[17 => 52 => 26 => 13 => 40 => 20 => 10 => 5 => 16 => 8 => 4 => 2 => 1]
显示的函数不仅适用于默认的 STL 容器,如std::vector
、 std::list
、 std::deque
、 std::string
等,还适用于提供类似接口的自定义容器。
在可能的情况下,FunctionalPlus 会为您推断出类型。让我们从 Collatz 示例中取出一行代码:
auto show_collats_seq = fplus::compose(collatz_seq, show_ints);
collatz_seq
是一个采用uint64_t
并返回list
的函数。 show_ints
接受一个list
并返回一个string
。通过使用 kennyim 编写的function_traits
,可以自动将表达式fplus::compose(collatz_seq, show_ints)
推导为采用uint64_t
并返回string
函数,因此您不必手动提供类型提示编译器。
如果传入的两个“连接类型”不匹配的函数,将生成一条描述问题的明确错误消息。 FunctionPlus 使用编译时断言来避免编译器在函数模板中遇到类型错误时生成令人困惑的长错误消息。
将编程方式从“编写自己的循环和嵌套 if”更改为“编写和使用小函数”将导致编译时出现更多错误,但运行时错误会减少。此外,更精确的编译时错误将减少调试时间。
“使用FunctionalPlus 库在C++ 中进行函数式编程;今天:HackerRank 挑战宝石”一文通过展示如何使用FunctionalPlus 方法开发针对问题的优雅解决方案,对该库进行了流畅的介绍。
Udemy 上还有一门课程“使用 C++ 进行函数式编程”,该课程大量使用 FunctionPlus 来解释一般函数概念。
上面的“宝石”教程解释了如何应用函数式思维来解决以下问题:
查找输入文本的每一行中存在的字符数。
std::string gemstone_count ( const std::string& input)
{
using namespace fplus ;
typedef std::set characters;
const auto lines = split_lines ( false , input); // false = no empty lines
const auto sets = transform (
convert_container,
lines);
// Build the intersection of all given character sets (one per line).
const auto gem_elements = fold_left_1 (
set_intersection, sets);
return show ( size_of_cont (gem_elements));
}
通过使用namespace fwd
中的功能,您可以在没有临时变量的情况下进行操作,并清楚地表明整个过程只是通过一系列函数推送输入,类似于 Unix 命令行中的管道概念。
std::string gemstone_count_fwd_apply ( const std::string& input)
{
using namespace fplus ;
typedef std::set characters;
return fwd::apply (
input
, fwd::split_lines ( false )
, fwd::transform (convert_container)
, fwd::fold_left_1 (set_intersection)
, fwd::size_of_cont ()
, fwd::show ()
);
}
在fplus::fwd::
中,您再次发现许多fplus::
函数,但在部分柯里化版本中,即fplus::foo : (a, b, c) -> d
与fplus::foo : (a, b) -> (c -> d)
。这使得上面的样式成为可能。
除了正向应用程序版本之外,您还可以编写无点并通过组合定义函数:
using namespace fplus ;
typedef std::set characters;
const auto gemstone_count_fwd_compose = fwd::compose(
fwd::split_lines ( false ),
fwd::transform(convert_container),
fwd::fold_left_1(set_intersection),
fwd::size_of_cont(),
fwd::show()
);
顺便说一句,如果您需要相反顺序的二元函数的参数, namespace fplus::fwd::flip
也存在。 fplus::bar : (a, b) -> c
不仅在fplus::fwd::bar : a -> b -> c
中有其类似物,而且在fplus::fwd::flip::bar : b -> a -> c
中也有类似物fplus::fwd::flip::bar : b -> a -> c
。
如果您正在寻找特定的FunctionalPlus 函数,但您还不知道其名称,您当然可以使用IDE 的自动完成功能来浏览namespace fplus
的内容。但推荐的方法是使用FunctionalPlus API搜索网站。您可以使用它通过关键字或函数类型签名快速搜索。如果您愿意,还可以使用 Sourcegraph 浏览源代码。
得益于 C++ 无开销的抽象概念,基本函数速度很快。以下是第一个示例的一些测量结果,在标准台式 PC 上进行,使用 GCC 和O3
标志编译。
5000 random numbers, keep odd ones, 20000 consecutive runs accumulated
----------------------------------------------------------------------
| Hand-written for loop | std::copy_if | fplus::keep_if |
|-----------------------|--------------|----------------|
| 0.632 s | 0.641 s | 0.627 s |
因此,编译器似乎在优化和内联所有内容方面做得非常好,以使其在机器代码性能方面基本相等。
但有时可以以更优化的方式编写更复杂的函数。如果您在性能关键场景中使用FunctionalPlus,并且分析显示您需要更快的函数版本,请告诉我,甚至帮助改进FunctionalPlus。
如果给定容器是右值(例如在链式调用中),FunctionalPlus 内部通常可以就地操作,从而避免许多不必要的分配和复制。但并非所有情况都是如此。然而,由于使用多范式语言,我们可以轻松地将手动优化的命令式代码与fplus
函数结合起来。幸运的是,经验(又名分析)表明,在大多数情况下,应用程序中的绝大多数代码与整体性能和内存消耗无关。因此,最初关注开发人员的生产力和代码的可读性是一个好主意。
FunctionPlus 和 range-v3(C++-20 中ranges
的基础)确实有一些共同点,如以下代码片段所示。
const auto times_3 = []( int i){ return 3 * i;};
const auto is_odd_int = []( int i){ return i % 2 != 0 ;};
const auto as_string_length = []( int i){ return std::to_string (i). size ();};
// FunctionalPlus
using namespace fplus ;
const auto result_fplus = fwd::apply(
numbers ( 0 , 15000000 )
, fwd::transform(times_3)
, fwd::drop_if(is_odd_int)
, fwd::transform(as_string_length)
, fwd::sum());
// range-v3
const auto result_range_v3 =
accumulate (
views::ints ( 0 , ranges::unreachable)
| views::take( 15000000 )
| views::transform(times_3)
| views::remove_if(is_odd_int)
| views::transform(as_string_length), 0);
但也存在一些差异。 Range-v3 范围是惰性的,这意味着在如上所述的处理链的单个步骤中不会分配中间内存。另一方面,当使用FunctionalPlus 时,您可以使用普通的STL 容器。与编写新的范围适配器相比,实现新功能也更简单。此外,FunctionalPlus 提供了更多开箱即用的功能,并具有 API 搜索网站。因此,这两个库之间的选择取决于您的偏好和项目的需求。
需要C++14兼容的编译器。这些版本的编译器都很好:
安装FunctionalPlus 的不同方法的指南可以在INSTALL.md 中找到。
这个库中的功能最初是由于我在经常使用 C++ 时对它的个人需求而增长的。我尽最大努力使其没有错误并尽可能使用起来舒适。 API 将来仍可能发生变化。如果您有任何建议、发现错误、错过某些功能或想要提供一般反馈/批评,我很乐意听取您的意见。当然,也非常欢迎贡献。
根据 Boost 软件许可证版本 1.0 分发。 (请参阅随附文件LICENSE
或复制 http://www.boost.org/LICENSE_1_0.txt)