幫助您編寫簡潔易讀的 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)