C++
int pay( int hour )
{
if( hour <= 3 )
{
return hour*30;
}
else
{
return 3*30 + (hour-3)*20;
}
}
int main()
{
int n;
while( cin >> n )
{
cout << pay(n) << endl;
}
return 0;
}
函式一旦執行到return,就會立刻回傳,略過之後所有程式碼
在呼叫函式時,提供給函式的資料稱為引數(argument),接受引數的稱為參數(parameter)。例如以下的範例, n 是 reference,型態為int,呼叫函式時提供 x 作為argument:
#include <iostream>
using namespace std;
int increment(int n) {
n = n + 1;
return n;
}
int main() {
int x = 10;
cout << increment(x) << endl;
cout << x << endl;
return 0;
}
輸出:
11
10
可以看見, n 雖然作了遞增運算,但是對 x 的儲存值沒有影響,x 最後仍是顯示 10,這是因為當我們呼叫function時,並不會真正改變我們傳進去的值。 在function中他會建一個新的變數(n),值與我們傳進的值(x)一樣。 若想真正改變到傳入的值,我們必須傳入變數的記憶體位址,直接對那塊記憶體位址的值進行操作,如下:
#include <iostream>
using namespace std;
int increment(int *n) {
*n = *n + 1;
return *n;
}
int main() {
int x = 10;
cout << increment(&x) << endl;
cout << x << endl;
return 0;
}
又或者,我們可以傳入變數的Reference,如下:
#include <iostream>
using namespace std;
int increment(int &n) {
n = n + 1;
return n;
}
int main() {
int x = 10;
cout << increment(x) << endl;
cout << x << endl;
return 0;
}
輸出:
11
11
在這邊要回顧一下 Rvalue Reference 中談到的,為何會需要使用 const int &r = 10 這種語法,因為 lvalue 不能為常量的Reference,底下範例會編譯失敗:
#include <iostream>
using namespace std;
int foo(int &n) {
return n + 1;
}
int main() {
int x = 10;
foo(x);
foo(10); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
return 0;
}
若要 foo 呼叫都能通過編譯,foo 的參數必須以 const int &n 來宣告:
#include <iostream>
using namespace std;
int foo(const int &n) {
return n + 1;
}
int main() {
int x = 10;
foo(x);
foo(10); // OK
return 0;
}
C++ 11 開始可以使用 Rvalue Reference,參數也可以宣告 Rvalue Reference,當兩個函式各定義了 Rvalue Reference 與 Const Lvalue Reference 作為參數,使用常量呼叫時,編譯器會選擇 Rvalue Reference的版本:
#include <iostream>
using namespace std;
void foo(int &&n) {
cout << "rvalue ref" << endl;
}
void foo(const int &n) {
cout << "lvalue ref" << endl;
}
int main() {
foo(10); // 顯示 rvalue ref
return 0;
}
參數以 Rvalue Reference宣告的情況,主要考慮的是效率,在函式內容的實作上往往也就有別於 const 的 Lvalue Reference之版本,例如搭配 std::move 來實現移動語義(move semantics),這後面會再介紹。
前面有提到,在定義函式時,必須定義回傳值型態。 回傳值型態也可以為指標、Lvalue Reference 、 Rvalue Reference等。
如果回傳位址,那麼回傳值型態可定義為指標型態,這代表那塊記憶體位址在函式執行完畢後不能被刪除,也就是說,要在函式內動態配置記憶體,如:
#include <iostream>
using namespace std;
int* makeArray(int m, int initial = 0) {
int *a = new int[m];
for(int i = 0; i < m; i++) {
a[i] = initial;
}
return a;
}
void deleteArray(int* arr) {
delete [] arr;
}
int main() {
int m = 0;
cout << "陣列大小: ";
cin >> m;
int *arr = makeArray(m);
for(int i = 0; i < m; i++) {
cout << "arr[" << i << "] = " << arr[i] << endl;
}
deleteArray(arr);
return 0;
}
輸出:
陣列大小: 5
arr[0] = 0
arr[1] = 0
arr[2] = 0
arr[3] = 0
arr[4] = 0
回傳值型態也可以為Lvalue Reference或Rvalue Reference,但是一樣,不能將區域變數以Lvalue Reference傳回,或者將常量以Rvalue Reference傳回,因為函式執行完後,區域變數或常量的記憶體就會被刪除了。
在定義回傳值型態為Lvalue Reference時通常是為了效率。 像是下面的longerStr,在回傳引數或回傳值時,都會需要複製string的內容:
#include <iostream>
using namespace std;
string longerStr(string s1, string s2) {
return s1.length() > s2.length() ? s1 : s2;
}
int main() {
string s1 = "Justin Lin";
string s2 = "Monica Huang";
string s3 = longerStr(s1, s2);
cout << s3 << endl;
return 0;
}
因為longerStr只是要比較兩個string的長度,因此複製的東西能少則少,我們可以這樣修改:
#include <iostream>
using namespace std;
string& longerStr(string &a, string &b) {
return a.length() > b.length() ? a : b;
}
int main() {
string s1 = "Justin Lin";
string s2 = "Monica Huang";
string &s3 = longerStr(s1, s2);
cout << s3 << endl;
return 0;
}
上面這個例子,在longerStr中,a會被設為s1的Reference,b會被設為s2的Reference,回傳值的型態也為Reference,如此一來Reference會建一個新的string指到原來string的記憶體位子上,實際上是多了一個變數指到同一個地方,雖然還是需要複製記憶體位置,但是並不用複製整個字串,因此效率上快了許多。
類似地,定義傳回值型態為 Rvalue Reference,通常也是為了效率。例如:
#include <iostream>
using namespace std;
string&& concat(string &&lhs, string &rhs) {
lhs += rhs;
return std::move(lhs);
}
int main() {
string s = "++";
string result = concat("C", s);
cout << result << endl;
return 0;
}
輸出: C++
在這個例子中,引數 "C" 是個常量,參數 lhs 接管了該常量,因為函式執行完之後,lhs 生命周期也就結束,不會再使用,使用 std::move 將 lhs 當成是 rvalue 傳回,因此 lhs 的內容將移動至 result,而不是複製至 result,如果最後傳回的 lhs 是個很長的字串,效率上會比較好。
前面我們在宣告函數時都先把Parameter的數量固定了。 但如果函式想要能接受不定長度的argument(Variable-length argument)時該怎麼辦呢?。 我們可以使用 vector 定義參數,而呼叫方使用 vector 收集argument後,再來呼叫函式,例如:
#include <iostream>
#include <vector>
using namespace std;
void foo(vector<double>);
int main() {
vector<double> args;
args.push_back(1.1);
args.push_back(2.2);
args.push_back(3.3);
foo(args);
return 0;
}
void foo(vector<double> args) {
for(auto arg : args) {
cout << arg << endl;
}
}
輸出:
1.1
2.2
3.3
C++ 11 有兩個方案可以呼叫不訂長度引數的特定語法,一是定義參數型態為 initializer_list,透過清單初始化(list initialization)令呼叫函式的語法更方便一些;另一個方式是透過可變參數模版(variadic template)來定義,這需要認識模版語法等更多細節,之後再來談。
initializer_list 定義於 initializer_list 標頭檔,來看看如何使用:
#include <iostream>
#include <initializer_list>
using namespace std;
void foo(initializer_list<double>);
int main() {
foo({1.1, 2.2, 3.3});
return 0;
}
void foo(initializer_list<double> args) {
for(auto arg : args) {
cout << arg << endl;
}
}
輸出:
1.1
2.2
3.3
在呼叫函式時,使用了清單初始化 { } 包含了引數,實際上,如果 foo 定義參數時使用 vector,這個範例也可以運作,那為何要改為 initializer_list?因為清單初始化 { } 會建立 initializer_list,而 vector 則是有個建構式,可以接受 initializer_list,所以 vector 才也可以使用清單初始化。
簡單來說,只是想定義不定長度引數時,用initializer_list 就可以了,不過它包含的方法比較少,如果需要 vector 的方法,使用 vector 當然也是可以。
有時我們會撰寫有相同演算流程的函式,雖然參數型態不同,但物件的協定都相同,像是:
bool greaterThan(int a, int b) {
return a > b;
}
bool greaterThan(string a, string b) {
return a > b;
}
這時我們就可以使用函式模板,讓上方a,b的形態可以在呼叫時指定,而不是如上寫死了兩個版本:
#include <iostream>
using namespace std;
template <typename T>
bool greaterThan(T a, T b) {
return a > b;
}
int main() {
// 顯示 0
cout << greaterThan(10, 20) << endl;
// 顯示 1
cout << greaterThan(string("xyz"), string("abc")) << endl;
return 0;
}
在這個範例中,greaterThan 是個函式模版(function template),或稱為泛型函式(generic function),定義模版時使用 template,之後跟著模版參數列,typename 定義了一個模版參數 T,若有多個模版參數,各自都要使用 typename 來定義,每個模版參數以逗號區隔。
程式碼 greaterThan(10, 20) 建立了一個模版的實例(instance),相當於 greaterThan\<int>(10, 20)
,而 greaterThan(string("xyz"), string("abc"))
建立了另一個模版實例,相當於 greaterThan(string("xyz"), string("abc"))
。
建立一個模版實例的意思是,編譯器推斷出 T 的型態,產生並編譯了一個對應版本,這就是之所以名為模版的原因,也就是說編譯器以你定義的模版為基礎,為 greaterThan(10, 20) 建立了 bool greaterThan(int, int),為 greaterThan(string("xyz"), string("abc")) 建立了 bool greaterThan(string, string)。
但如果有某個版本,不想要編譯器建立,想要自行實作呢?那我們可以明確地定義特化版本:
#include <iostream>
using namespace std;
template <typename T>
bool greaterThan(T a, T b) {
return a > b;
}
template <>
bool greaterThan(string s1, string s2) {
return s1.size() > s2.size();
}
int main() {
cout << greaterThan(10, 20) << endl;
cout << greaterThan(string("xyz"), string("abc")) << endl;
return 0;
}
在這個例子中,也許你想比的是字串的長度而不是字典順序,為此建立了特化版本,因此編譯器就不會自行建立 bool greaterThan(string, string) 的版本,而是使用你定義的特化版本,執行結果就都顯示 0 了。
現在來看看一個需求,傳遞陣列給函式時會使用指標,一個例子是:
#include <iostream>
using namespace std;
void printAll(int *arr, int len) {
for(int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
int main() {
int arr1[] = {1, 2};
int arr2[] = {3, 4, 5};
printAll(arr1, 2);
printAll(arr2, 3);
return 0;
}
但如果想在 printAll 中使用 for range 可行嗎?就上例來說沒辦法,因為兩個陣列的長度不一樣長,因此將陣列的指標傳送過去時兩者的型態會不同(int(*)[2] 與 int(*)[3]),若要使用 for range,需要改為以下:
#include <iostream>
using namespace std;
void printAll(int (*arr)[2]) {
for(auto elem : *arr) {
cout << elem << " ";
}
cout << endl;
}
void printAll(int (*arr)[3]) {
for(auto elem : *arr) {
cout << elem << " ";
}
cout << endl;
}
int main() {
int arr1[] = {1, 2};
int arr2[] = {3, 4, 5};
printAll(&arr1);
printAll(&arr2);
return 0;
}
問題是解決了,然而也在參數上寫死了陣列長度,仔細看看兩個函式的實作內容是相同的,這時你會想,編譯器可以為函式模版推斷出對應型態的版本,那可否推斷出陣列長度的值呢?答案是可以XD!
#include <iostream>
using namespace std;
template <typename T, int L>
void printAll(T (*arr)[L]) {
for(auto elem : *arr) {
cout << elem << " ";
}
cout << endl;
}
int main() {
int arr1[] = {1, 2};
int arr2[] = {3, 4, 5};
printAll(&arr1);
printAll(&arr2);
return 0;
}
在上例中,L 並不是以 typename 定義,而是 int,這稱為模版的非型態參數(nontype parameter),編譯器會試著為非型態參數推斷出一個值,值的推斷來源必須是個常數運算式,也就是靜態時期可決定的值。
上例可以使用參考,令呼叫時更直覺一些,例如:
#include <iostream>
using namespace std;
template <typename T, int L>
void printAll(T (&arr)[L]) {
for(auto elem : arr) {
cout << elem << " ";
}
cout << endl;
}
int main() {
int arr1[] = {1, 2};
int arr2[] = {3, 4, 5};
printAll(arr1);
printAll(arr2);
return 0;
}
但實際上,T (&arr)[L] 的宣告是多此一舉,既然都用了模版了,底下照樣也行得通:
#include <iostream>
using namespace std;
template <typename T>
void printAll(T &arr) {
for(auto elem : arr) {
cout << elem << " ";
}
cout << endl;
}
int main() {
int arr1[] = {1, 2};
int arr2[] = {3, 4, 5};
printAll(arr1);
printAll(arr2);
return 0;
}
使用模板時,parameter不只可以是 lvalue,也可以是 rvalue。
先從簡單的開始,現在我們可以知道底下會顯示10:
#include <iostream>
using namespace std;
void foo(int &p) {
p = 10;
}
int main() {
int x = 5;
foo(x);
cout << x << endl;
return 0;
}
接下來,我們會使用模版,模版的流程中會呼叫 foo,底下結果會顯示什麼呢?
#include <iostream>
using namespace std;
void foo(int &p) {
p = 10;
}
template <typename T>
void some(T t) {
foo(t);
}
int main() {
int x = 5;
some(x);
cout << x << endl;
return 0;
}
因為 some(x) 的呼叫,編譯器建立了 some(int) 的版本,而不是 some(int&),因此結果會顯示 5。
若我們想要改動x的值時,模板該這樣定義:
template <typename T>
void some(T &t) {
foo(t);
}
這樣的話剛剛的範例就會顯示 10了。 但是這樣一來,就無法使用some(10)這種方式呼叫了,因為10是個rvalue。 也許我們可以利用重載來寫:
#include <iostream>
using namespace std;
void foo(int &p) {
p = 10;
}
template <typename T>
void some(T &t) {
foo(t);
}
template <typename T>
void some(const T &t) {
foo(t);
}
int main() {
int x = 5;
some(x);
some(10);
cout << x << endl;
return 0;
}
這樣的確行的通,some(10)也能夠呼叫了,只不過,模板的實作內容一模一樣,這樣似乎就失去了我們使用模板意義了。 在前面確實有個範例是將 greaterThan 模版特化出 greaterThan(string, string) 版本,然而其意義在於,特化版本的實作內容與泛型版本不同,但上面這個範例,顯然地,兩個模版的實作內容是相同的。
那麼到底該怎麼做呢? 可以改為以下的寫法:
#include <iostream>
using namespace std;
void foo(int &p) {
p = 10;
}
template <typename T>
void some(T &&t) {
foo(t);
}
int main() {
int x = 5;
some(x);
some(10);
cout << x << endl;
return 0;
}
嗯?呼叫 some(10) 時,10 是個 rvalue,因此 T&& 可以接受,這部份是沒問題。 但是 some(x)呢? x 不是個 lvalue 嗎?為什麼行得通呢?
這邊其實是 C++ 語言中的一個特例,如果將 lvalue 傳給模版函式的 T&& 參數的話,T 會被推斷為 int&,也就是說編譯器首先會為 some(x) 建立 some(int& &&) 版本!
那我們能不能自己寫個int& &&r之類的宣告呢? 建立一個參考的參考(reference to reference)? 就像指標的指標那樣。 很可惜,答案是不行的XD 想要建立參考的參考,必須要透過剛剛編譯器為some(x)建立some(int& &&)的方式,再由編譯器將int& &&收合為int&。
簡單來說,只有編譯器可以建立reference to reference,或者可以說,我們在C++內要建立參考的參考,就是透過模板「間接」建立的。
雖說是編譯器的權能,不過總得給個收合的規則吧!對於一個模版參數 t,編譯器推斷出型態後,會依以下情況收合:
因此剛剛 int& && 就收合為 int& 了,也就是個 lvalue 參考,編譯就通過了。
為了效率以及實作移動語義時的方便,C++ 11 可以建立 rvalue 參考,程式語言就是這樣,為了某個需求創造了新的語法,新的語法又會創造新的需求,然後循環就開始了,語言就越發膨脹而臃腫…來看看吧!以下會編譯失敗:
#include <iostream>
using namespace std;
void foo(int &&p) {
//...
}
template <typename T>
void some(T &&t) {
foo(t); // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
}
int main() {
some(10);
return 0;
}
編譯器建立了個 some(int&&) 版本,因此呼叫 some(10) 沒問題,可是 t 是個 lvalue,而現在 foo 的 p 是個 rvalue 參考,因此編譯失敗了。
但t 的運算式來源明明就是個 rvalue,編譯器不能直接 rvalue 的性質轉給 foo 的 p 嗎?它是做得到,只不過它不知道你要不要這麼做,如果你要這麼做,需要這樣寫:
#include <iostream>
#include <utility>
using namespace std;
void foo(int &&p) {
//...
}
template <typename T>
void some(T &&t) {
foo(std::forward<T>(t));
}
int main() {
some(10);
return 0;
}
utility 標頭檔中定義了 forward,不過這名稱太尋常了,建議呼叫時使用 std::forward 以避免同名問題,std::forward 是在告訴編譯器,將呼叫時運算式來源的資訊轉給接收的那方,就上例而言,可以看成 std::forward 建立了一個管道,接通了 10 與 int &&p,10 是個 rvalue,而 p 是個 rvalue 參考,這樣就 OK 了!
std::forward 是在告訴編譯器,將呼叫時運算式來源的資訊轉給接收的那方,因此不僅適用於以上的情況,例如,下面這樣是可以通過編譯的:
#include <iostream>
#include <utility>
using namespace std;
void r(int &p) {
}
void rr(int &&p) {
}
template <typename F, typename T>
void some(F f, T &&t) {
f(std::forward<T>(t));
}
int main() {
int x = 10;
some(r, x);
some(rr, 10);
return 0;
}
在這邊運用了傳遞函式,這之後就會說明,簡單來說,函式是可以傳遞的,在 some(r, x) 時,編譯器會建立 some(void (*f)(int&), int& && t) 的版本,也就是 T 被推斷為 int&,而後 int& &&t 會被收合為 int&,接著的 f(std::forward<T>(t)) 內容編譯器會建立為 f(std::forward<int&>(t)),也就是說,可以看成 x 與 int &p 之間建立了一個管道,因此可以通過編譯。
至於 some(rr, 10) 時,編譯器會建立 some(void (*f)(int&&), int &&t) 的版本,接著的 f(std::forward<T>(t)) 內容編譯器會建立為 f(std::forward<int>(t)),也就是說,可以看成 10 與 int &&p 之間建立了一個管道,因此可以通過編譯。
如果引數的個數無法事先確定,而且引數的型態可能各不相同,C++ 11 以後可以透過可變參數模版(variadic template)來解決。
#include <iostream>
using namespace std;
template <typename... Types>
void foo(Types... params) {
cout << sizeof...(Types) << " "
<< sizeof...(params) << endl;
}
int main() {
foo(1); // 顯示 1 1
foo(1, "XD"); // 顯示 2 2
foo(1, "XD", 3.14); // 顯示 3 3
return 0;
}
typename 之後接續了省略語法 …,這可以看成 Types 代表不定長度的 typename T1, typename T2, …,Types 被稱為模版參數包(template parameter pack),宣告參數時,Types… params 代表了不定長度的 T1 t1, T2 t2, …,params 被稱為函式參數包(function parameter pack)。
可以使用 sizeof… 來得知實際呼叫時的型態數量或引數數量,這個值是編譯時期推斷得知的,根據範例中呼叫的方式,在編譯時期 foo 會被實例出 foo(int)、foo(int, const char*) 與 foo(int, const char*, double) 版本,因此 params 並不是個物件,那麼該怎麼取得呼叫時的引數呢?
如果呼叫時的引數是同一型態,一個簡單的方式是展開為陣列、vector 等型態。例如:
#include <iostream>
#include <vector>
using namespace std;
template <typename T, typename ...Ts>
T sum(T first, Ts... rest) {
vector<T> nums = {rest...};
T r = first;
for(auto n : nums) {
r += n;
}
return r;
}
int main() {
cout << sum(1, 2, 3) << endl;
cout << sum(1, 2, 3, 4, 5) << endl;
return 0;
}
在編譯時期,上面的範例會產生 sum(int, int, int) 與 sum(int, int, int, int, int) 兩個版本,而 {rest…} 用來解開參數包,解開之意是指 {rest…} 會分別產生 {p1, p2, p3} 與 {p1, p2, p3, p4, p5}(p1 等名稱代表參數)。
但如果實際上傳遞的引數型態各不相同,又該怎麼辦呢? 這時得使用遞迴並配合解開參數包。例如:
#include <iostream>
using namespace std;
template <typename T>
void print(T p) {
cout << p << endl;
}
template <typename T, typename ...Ts>
void print(T first, Ts... rest) {
cout << first << " ";
print(rest...);
}
int main() {
print(1);
print(1, "2");
print(1, "2", 3.14);
return 0;
}
print(1) 會產生一個 print(int) 版本,這沒有問題;print(1, "2") 的產生一個 print(int, const char*) 版本,然後 print(rest..) 的部份會解開為 print("2"),這又會產生出 print(const char*),像這樣:
void print(const char* p) {
cout << p << endl;
}
void print(int p1, const char* p2) {
cout << p1 << " ";
print(p2);
}
這就是為何可變參數模版可以接受不同型態引數的原因了,依以上的說明,print(1, "2", 3.14) 最後會產生出以下的版本:
void print(double p) {
cout << p << endl;
}
void print(const char* p1, double p2) {
cout << p1 << " ";
print(p3);
}
void print(int p1, const char* p2, double p3) {
cout << p1 << " ";
print(p2, p3);
}
在 Parameter pack 內就舉了個實作 tprintf 函式的例子:
#include <iostream>
void tprintf(const char* format) // base function
{
std::cout << format;
}
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // recursive variadic function
{
for ( ; *format != '\0'; format++ ) {
if ( *format == '%' ) {
std::cout << value;
tprintf(format+1, Fargs...); // recursive call
return;
}
std::cout << *format;
}
}
int main()
{
tprintf("% world% %\n","Hello",'!',123);
return 0;
}
因為可變參數模版可以使用的引數會是各種型態,若各種不同型態有各自的處理方式,那就得知道目前處理的資料是哪種型態,而以上的例子也暗示了,可以像 C 的 printf,在格式上指定 %d、%s 等,提供資訊以進一步決定各個引數的型態為何,以進行相對應的處理。
在解開參數包的同時,可以指定套用某個函式,例如:
#include <iostream>
#include <vector>
using namespace std;
template<typename T>
T sum(T first) {
return first;
}
template<typename T, typename... Ts>
T sum(T first, Ts... params) {
return first + sum(params...);
}
template<typename T>
T doubleIt(T t) {
return t + t;
}
template<typename T, typename... Ts>
T doubleSum(T first, Ts... params) {
return doubleIt(first) + sum(doubleIt(params)...);
}
int main() {
cout << sum(1, 2) << endl; // 3
cout << sum(string("1"), string("2")) << endl; // 12
cout << doubleSum(1, 2) << endl; // 6
cout << doubleSum(string("1"), string("2")) << endl; // 1122
return 0;
}
以前我們宣告函式時都是把回傳型態寫在最前面。 但從C++11後新增了一個寫法,就是把回傳型態寫再函式原型的後面,像這樣:
auto add(int a, int b) -> int {
return a + b;
}
上面這樣寫其實就只是說回傳型態是int,auto並沒有作用,但你可能想問,那寫在後面還有什麼用勒?
講這個之前要先提一下decltype這個東西,他和auto一樣,可以用來宣告型別,用法是
decltype(expression)
expression 可能是一個變數,或函式之類的,編譯器便會去利用expression去推導出一個型態,但要注意,它並不會去執行裡面的 expression,可以看一下這個例子:
int a = 0;
decltype(a) b = 1;
decltype(10.8) c = 5.5;
decltype(x + 100) d;
在這個例子中 decltype(a)
就會被轉換為 int ,因此 b 的型態就為int,同理,c 為double,d為double。
那編譯器在推導 decltype(expression) 的時候,有三條規則:
()
包圍的表達式,或者是一個Class成員訪問的表達式,又或是一個單獨的量(像是 2 這類的),那 decltype(expression) 的型態就和 expression 一樣,這也是最常見的狀況()
包圍,那 decltype(expression) 的型態就是 expression 的 reference。接下來看看這個例子:
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
auto addThese(T begin, T end) {
auto r = *begin;
for(auto it = begin + 1; it != end; it++) {
r += *it;
}
return r;
}
int main() {
vector<int> vt = {1, 2, 3};
cout << addThese(vt.begin(), vt.end()) << endl;
vector<string> vt2 = {"Justin", "Monica", "Irene"};
cout << addThese(vt2.begin(), vt2.end()) << endl;
return 0;
}
addThese 傳回迭代器範圍內元素的加總,就範例來說,因為 vt 是 vector<int>,因此元素型態是 int,現在問題來了,如果想在標頭檔宣告函式原型呢?這樣行不通:
template <typename T>
auto addThese(T begin, T end);
因為沒有程式碼上下文,是要怎麼 auto 傳回型態呢?就範例來說,*begin 的型態就是 int,那麼這樣呢?
template <typename T>
decltype(*begin) addThese(T begin, T end);
這樣也行不通,因為編譯器剖析程式碼到 decltype(*begin)時,根本還沒看到 begin 參數,像這種情況,可以使用尾端傳回型態(tailing return type):
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
auto addThese(T begin, T end) -> decltype(*begin + *end);
int main() {
vector<int> vt = {1, 2, 3};
cout << addThese(vt.begin(), vt.end()) << endl;
return 0;
}
template <typename T>
auto addThese(T begin, T end) -> decltype(*begin + *end) {
auto r = *begin;
for(auto it = begin + 1; it != end; it++) {
r += *it;
}
return r;
}
在範例中,編譯器已經看到了參數 begin 與 end,之後使用 -> decltype(*begin + *end) 就沒問題,那為什麼不用 decltype(*begin) 呢?
因為 *begin 是個 lvalue,若迭代器中的元素型態是 T,那 decltype(*begin) 會推斷出 T&,這樣的話,傳回型態會參考區域變數 r,然而函式執行完後 r 就無效了,因此不能使用 decltype(*begin);這邊需要的是個 rvalue,以令其推斷出 T,因此使用 decltype(*begin + *end)。
另一個會用到的場合,可能是在遇到底下的情況時:
class Foo {
public:
class Orz {
};
Orz* orz();
};
Foo::Orz* Foo::orz() {
return new Foo::Orz();
}
之後會談到類別定義,簡單來說,這邊定義了一個巢狀類別,在實作 orz 成員函式時,必須得以 Foo::Orz* 來指明傳回型態,因為必須知道是在哪個類別中的內部類別,然而,可以使用尾端傳回型態來簡化:
class Foo {
public:
class Orz {
};
Orz* orz();
};
auto Foo::orz() -> Orz* {
return new Foo::Orz();
}
因為 auto 後的 Foo:: 已經指明了外部類別了,尾端傳回型態時就可以直接指定 Orz*,不用再加上 Foo::。
簡單來說,尾端傳回型態既有的型態,或者取用 -> 前看過的相關名稱,用以宣告或簡化傳回型態。
程式在執行時,函式在記憶體中也佔有一個空間,將函式名稱作為指定來源時,函式名稱會自動轉為指標,型態由傳回值型態與參數列決定,若要將之指定給另一函式指標,型態的宣告方式如下:
傳回值型態 (*名稱)(參數列);
函式指標代表著一個函式,相同型態的函式可以指定給具有相同型態的指標,例如:
#include <iostream>
using namespace std;
int foo(int);
int main() {
int (*fp)(int) = foo;
foo(10); // 顯示 10
fp(20); // 顯示 20
return 0;
}
int foo(int n) {
cout << "n = " << n << endl;
return 0;
}
foo 指定給 fp,等效於 &foo 指定給 fp,在指定之後,fp 儲存了 foo 的位址,在呼叫時,fp(20) 等效於 (*fp)(20)。
重載函式具有不同的簽署,因此在指定時,雖然具有相同名稱,然而依函式指標的型態不同,編譯器會選擇對應的重載函式:
#include <iostream>
using namespace std;
void foo(int);
int foo(int, int);
int main() {
void (*fp)(int) = foo;
int (*add)(int, int) = foo;
foo(10);
cout << "1 + 2 = " << add(1, 2) << endl;
return 0;
}
void foo(int n) {
cout << "n = " << n << endl;
}
int foo(int a, int b) {
return a + b;
}
輸出:
n = 10
1 + 2 = 3
函式指標可以用來傳遞函式,例如,你想撰寫用於陣列的 sort 函式,希望大小順序可以由呼叫者指定,這就可以傳遞函式來指定,例如:
#include <iostream>
using namespace std;
void sort( int *, int, bool ( *compare )( int, int ) );
bool ascending( int, int );
bool descending( int, int );
int main() {
int number[] = { 3, 5, 1, 6, 9 };
sort( number, 5, ascending );
// 顯示 1 3 5 6 9
for ( auto n : number ) {
cout << n << " ";
}
cout << endl;
sort( number, 5, descending );
// 顯示 9 6 5 3 1
for ( auto n : number ) {
cout << n << " ";
}
cout << endl;
return 0;
}
void swap( int &a, int &b ) {
int t = a;
a = b;
b = t;
}
void sort( int *arr, int length, bool ( *compare )( int, int ) ) {
for ( int flag = 1, i = 0; i < length - 1 && flag == 1; i++ ) {
flag = 0;
for ( int j = 0; j < length - i - 1; j++ ) {
if ( compare( arr[j + 1], arr[j] ) ) {
swap( arr[j + 1], arr[j] );
flag = 1;
}
}
}
}
bool ascending( int a, int b ) {
return a < b;
}
bool descending( int a, int b ) {
return a > b;
}
在這個例子中,sort 上的函式指標宣告有些難以閱讀,可以使用 typedef,定義一個比較容易閱讀的名稱,例如:
#include <iostream>
using namespace std;
typedef bool (*CMP)(int, int);
void sort(int*, int, CMP);
...略
void sort(int* arr, int length, CMP compare) {
for(int flag = 1, i = 0; i < length - 1 && flag == 1; i++) {
flag = 0;
for(int j = 0; j < length - i - 1; j++) {
if(compare(arr[j + 1], arr[j])) {
swap(arr[j + 1], arr[j]);
flag = 1;
}
}
}
}
...略
C++ 11後鼓勵使用 using 來取代 typedef,因為比較直覺,而且可以結合模版。例如:
using CMP = bool (*)(int, int);
另一個方式是使用 decltype,這可以就一個既有的型態來進行型態宣告。例如:
#include <iostream>
using namespace std;
bool cmp(int, int);
void sort(int*, int, decltype(cmp));
bool ascending(int, int);
bool descending(int, int);
...略
void sort(int* arr, int length, decltype(cmp) compare) {
for(int flag = 1, i = 0; i < length - 1 && flag == 1; i++) {
flag = 0;
for(int j = 0; j < length - i - 1; j++) {
if(compare(arr[j + 1], arr[j])) {
swap(arr[j + 1], arr[j]);
flag = 1;
}
}
}
}
...略
在參數的型態宣告複雜時,雖然不能使用 auto,decltype 的運用可以稍微緩解一下型態宣告的負擔。
也可以宣告函式指標陣列,例如:
bool (*compare[10])(int, int) = {nullptr};
上面這個宣告產生具有 10 個元素的陣列,可以儲存 10 個 bool (*)(int, int) 型態的函式位址,目前都初始為 nullptr,不過這樣的宣告實在難以閱讀,可以使用 using 來改進:
using CMP = bool (*)(int, int);
CMP compare[10] = nullptr;
若是使用 decltype 的話,必須是:
bool cmp(int, int);
decltype(cmp) *compare[10] = {nullptr};
傳遞函式時多半使用函式指標,不過也可以建立函式參考,例如:
#include <iostream>
using namespace std;
int foo(int);
int main() {
int (&fr)(int) = foo;
foo(10); // 顯示 10
fr(20); // 顯示 20
return 0;
}
int foo(int n) {
cout << "n = " << n << endl;
return 0;
}
fr 就只是 foo 的別名,也可以參數列上宣告函式參考:
#include <iostream>
using namespace std;
void sort( int *, int, bool ( &compare )( int, int ) );
bool ascending( int, int );
bool descending( int, int );
int main() {
int number[] = { 3, 5, 1, 6, 9 };
sort( number, 5, ascending );
// 顯示 1 3 5 6 9
for ( auto n : number ) {
cout << n << " ";
}
cout << endl;
sort( number, 5, descending );
// 顯示 9 6 5 3 1
for ( auto n : number ) {
cout << n << " ";
}
cout << endl;
return 0;
}
void swap( int &a, int &b ) {
int t = a;
a = b;
b = t;
}
void sort( int *arr, int length, bool ( &compare )( int, int ) ) {
for ( int flag = 1, i = 0; i < length - 1 && flag == 1; i++ ) {
flag = 0;
for ( int j = 0; j < length - i - 1; j++ ) {
if ( compare( arr[j + 1], arr[j] ) ) {
swap( arr[j + 1], arr[j] );
flag = 1;
}
}
}
}
bool ascending( int a, int b ) {
return a < b;
}
bool descending( int a, int b ) {
return a > b;
}
可以建立函式指標的陣列,然而,參考是別名,不是物件,無法建立參考的陣列(array of references),或許這是傳遞函式時,多半使用函式指標的原因。
C++ 11 提供了 function,定義於 functional 標頭檔,該類別的實例可以接受 Callable 物件,函式指標是其中之一,使用它來定義接受函式指標的參數,語法上會比較輕鬆一些,例如:
#include <functional>
#include <iostream>
using namespace std;
void sort( int *, int, function<bool( int, int )> compare );
bool ascending( int, int );
bool descending( int, int );
int main() {
int number[] = { 3, 5, 1, 6, 9 };
sort( number, 5, ascending );
// 顯示 1 3 5 6 9
for ( auto n : number ) {
cout << n << " ";
}
cout << endl;
sort( number, 5, descending );
// 顯示 9 6 5 3 1
for ( auto n : number ) {
cout << n << " ";
}
cout << endl;
return 0;
}
void swap( int &a, int &b ) {
int t = a;
a = b;
b = t;
}
void sort( int *arr, int length, function<bool( int, int )> compare ) {
for ( int flag = 1, i = 0; i < length - 1 && flag == 1; i++ ) {
flag = 0;
for ( int j = 0; j < length - i - 1; j++ ) {
if ( compare( arr[j + 1], arr[j] ) ) {
swap( arr[j + 1], arr[j] );
flag = 1;
}
}
}
}
bool ascending( int a, int b ) {
return a < b;
}
bool descending( int a, int b ) {
return a > b;
}
C++ 11 後可以使用 lambda 運算式,lambda 運算式定義了一個 Callable 物件,也就是個可以接受呼叫操作的物件,例如函式就是其中之一。
來看看 lambda 運算式的定義方式:
[=] (int x) mutable throw() -> int
{
// 函數內容
int n = x + y;
return n;
}
再來我們慢慢來看運算式裡面的每個部分,參考連結1 參考連結2
[ ]
:lambda-introducer,也稱為 capture clause
所有的 lambda expression 都是以它來作開頭的,不能省略!! 它除了用來作為 lambda expression 開頭的關鍵字之外,也有抓取(capture)變數的功能,指定該如何將目前 scope 範圍的變數抓進 lambda expression 中使用,而抓取變數的方式則分為傳值(by value)與傳參考(by reference)兩種,跟一般函數參數的傳入方式類似,不過語法有些不同,等等會有範例來解釋
[]
:只有兩個中括號,不抓取外部變數。[=]
:所有的變數都以傳值 (by value) 的方式抓取。[&]
:所有的變數都以傳參考 (by reference) 的方式抓取。[x, &y]
:x 以傳值的方式抓取,y 以傳參考的方式抓取。[=, &y]
:y 以傳參考的方式抓取,而 y 以外的變數都以傳值的方式抓取。[&, x]
:x 變數以傳值的方式抓去,而 x 以外的變數都以傳參考的方式抓取。要注意的是,預設的抓取方法(capture-default,也就是 =
或 &
)要放在所有項目前面,也就是放在第一個位置。
(int x)
:lambda declarator,也稱為參數清單(parameter list)
定義此匿名函數的傳入參數列表,基本的用法跟一般函數的傳入參數列表一樣,不過多了一些限制條件:
參數清單在 lambda expression 中並不是一個必要的項目,如果不需要傳入任何參數的話,可以連同小括號都一起省略。
mutable
:mutable specification
加入此關鍵字可以讓 lambda expression 直接修改以傳值方式抓取進來的外部變數,若不需要此功能,則可以將其省略。
throw()
:例外狀況規格 (exception specification)
指定該函數會丟出的例外,其使用的方法跟一般函數的例外指定方式相同。如果該函數沒有使用到例外的功能,則可以直接省略掉。
-> int
:傳回值型別 (return type)
指定 lambda expression 傳回值的型別,上面範例指定的傳回值型別為整數(int),其他的型別則以此類推。如果 lambda expression 所定義的函數很單純,只有包含一個傳回陳述式(statement)或是根本沒有傳回值的話,這部分就可以直接省略,讓編譯器自行判斷傳回值的型別。
回到我們在前面函式指標的範例,在那個範例內,定義了 ascending、descending 函式以便傳遞,如果事先這兩個函式並不存在,你想在 main 直接傳遞比序演算,C++ 11 以後可以如下:
#include <iostream>
#include <functional>
#include <algorithm>
using namespace std;
int main() {
int number[] = {3, 5, 1, 6, 9};
auto print = [](int n) { cout << n << " "; };
sort(begin(number), end(number), [](int n1, int n2) { return n2 - n1; });
// 顯示 9 6 1 5 3
for_each(begin(number), end(number), print);
cout << endl;
sort(begin(number), end(number), [](int n1, int n2) { return n1 - n2; });
// 顯示 3 5 1 6 9
for_each(begin(number), end(number), print);
cout << endl;
return 0;
}
上面的 sort 和 for_each 是定義在 algorithm 的函式,可以給它陣列開頭與結尾的位址,並傳遞一段演算,聲明想對陣列做些什麼,sort 是指定了比序的依據,而 for_each 指定了 print 定義的演算,也就是接受陣列元素值並顯示在標準輸出。
接下來我們來看一下這行:
auto print = [](int n) { cout << n << " "; };
這定義了一個 Callable 物件,呼叫時可以接受一個引數,因為沒有 return,也沒有定義 lambda 運算式的傳回型態,就自動推斷為 ret 的部份為 void,也就是相當於:
auto print = [](int n) -> void { cout << n << " "; };
那麼 print 的型態是什麼呢?lambda 運算式會建立一個匿名類別(稱為 closure type)的實例,因為無法取得匿名類別的名稱,也就無法宣告其型態,因而大多使用 auto 來自動推斷。
然而這就有一個問題,若要定義一個函式可以接受 lambda 運算式,參數無法使用 auto 呀! 那怎麼辦呢?可以包含 functional 標頭檔,使用 function 來宣告,function 的實例可以接受 Callable 物件,lambda 運算式是其中之一,例如:
function<void(int)> print = [](int n) { cout << n << " "; };
若 lambda 運算式被指定給函式指標,那麼 lambda 運算式建立的實例會轉換為位址:
void (*f)(int) = [](int n) { cout << n << " "; };
因此,既有的函式若參數是函式指標型態,也可以接受 lambda 運算式。
lambda 運算式的本體若有 return,然而沒有定義 return 的型態時,會自動推斷,因此底下 f1 return 的型態會自動推斷為 int:
auto f1 = [](int n1, int n2) { return n2 - n1; }
auto f2 = [](int n1, int n2) -> int { return n2 - n1; };
接下來看 [ ]
(lambda-introducer),前面有提到若只定義為 [ ]
時,沒辦法使用任何 lambda 運算式外部的變數,若想運用外部變數,定義時 [ ]
內必須要設定抓取變數的方法
若使用 [=]
,lambda 運算式本體內在取用到某外部變數時,其實是隱含地建立了同名、同型態的區域變數,然後將外部變數的值複製給區域變數,預設情況下不能修改,然而可以加上 mutable 修飾,不過要注意的是,這時修改的會是區域變數的值,不是外部變數。例如:
#include <iostream>
using namespace std;
int main() {
int x = 10;
auto f = [=]() mutable -> void {
x = 20;
cout << x << endl;
};
f(); // 顯示 20
cout << x << endl; // 顯示 10
return 0;
}
再看一個例子:
#include <functional>
#include <iostream>
int main()
{
using namespace std;
int i = 3;
int j = 5;
function<int (void)> f = [i, &j] { return i + j; };
// 改變i和j的值
i = 22;
j = 44;
// 呼叫f()輸出結果
cout << f() << endl; //輸出 : 47
}
注意這裡的輸出會是47,原因是在寫
function<int (void)> f = [i, &j] { return i + j; };
這行的時後,他就已經去抓現在的 i 和 &j了,因此 f 裡面的 i 在宣告時就已經確定了,而 f 裡面的 &j 則是一個參考,擁有外面的 j 上記憶體位址的存取更改權,因此在外面改動 j 時,f 內的j仍然會被改到。 換句話說,f 內的 i 與外面的 i 是不同的東西,但 j 卻是類似相同的東西 (或說相通比較正確)。
而和之前學函式的時候一樣,如果我們想要真正的更改 x 的值,需要使用參考,而若使用 [&]
, lambda 運算式本體內在參考外部變數時,其實是隱含地建立了同名的參考,因此在 lambda 運算式本體中修改變數,另一變數取值就也會是修改過的結果:
#include <iostream>
using namespace std;
int main() {
int x = 10;
auto f = [&]() mutable -> void {
x = 20;
cout << x << endl;
};
f(); // 顯示 20
cout << x << endl; // 顯示 20
return 0;
}
若有必要,lambda 運算式建立之後也可以馬上呼叫,例如:
#include <iostream>
using namespace std;
int main() {
// 顯示 Hello, Justin
[](const char *name) {
cout << "Hello, " << name << endl;
}("Justin");
return 0;
}
在定義模版(template)時,lambda 運算式也可以模版化。例如:
template <typename T>
function<T(T)> negate_all(T t1) {
return [=](T t2) -> T {
return t1 + t2;
};
}
在 C++ 14,捕捉變數時,可以建立新變數並指定其值,新變數的型態會自動推斷。例如:
auto print = [x = 10](int n) { cout << n + x << " "; };
雖然函式的參數型態不能以 auto 宣告,但在 C++ 14,lambda 運算式的參數型態可以是 auto:
#include <iostream>
using namespace std;
int main() {
auto plus = [] (auto a, auto b) {
return a + b;
};
// 顯示 3
cout << plus(1, 2) << endl;
// 顯示 abcxyz
cout << plus(string("abc"), string("xyz")) << endl;
return 0;
}
指定給 plus 的 lambda 運算式,稱為泛型 lambda(generic lambda),原理是基於模版,引數型態只要符合本體中的實作協定就可以用來呼叫 lambda 運算式,在上例中就是引數要能使用 + 運算子處理。
在一些語言中,若函式可以傳遞,該語言中會稱其一級函式(first-class function),就這點而言,C++ 早就具備,不過有些開發者認為,應該要包含可以建立匿名函式的能力,在語言才稱具有一級函式的特性,就這點來說,C++ 11 有了 lambda 運算式後,才算符合這點。
無論如何,C++ 現在無疑是具有一級函式的特性了,而在 algoritm 標頭檔中,定義了一些函式,可以接受函式或 lambda 運算式作為引數,在前面的例子我們就看過了 for_each、sort 和 find_if 的運用。
algoritm 中的東西很多,這邊只舉幾個常見的運用。
在 vector 內,若要尋找首個奇數該怎麼辦呢?可以使用 find_if,find_if會將迭代器停在第一個符合條件的地方,例如:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> number = {11, 12, 21, 30, 31, 41, 55, 66, 80, 98};
auto p = find_if(number.begin(), number.end(), [] (int n) { return n % 2; });
if(p != number.end()) {
cout << *p << endl;
}
else {
cout << "沒有奇數" << endl;
}
return 0;
}
輸出: 11
在 C++ 中,容器之類的操作常會涉及迭代器,因此使用上與其他具備一級函式特性的語言,在撰寫上較不直覺,然而,換來的是更大的彈性,因為只要是具有相同協定的結構,都可以套用這類操作。
以在一級函式的語言中,經常會舉 filter 之類的例子,如果 filter 出來的值是要保留在新的容器,可以使用 copy_if,例如:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> number = { 11, 12, 21, 30, 31, 41, 55, 66, 80, 98 };
vector<int> dest( number.size() );
auto destEnd = copy_if( number.begin(), number.end(), dest.begin(), []( int n ) { return n % 2; } );
for_each( dest.begin(), destEnd, []( int n ) { cout << n << " "; } ); // 11 21 31 41 55
return 0;
}
copy_if 第三個參數需要目標容器的迭代器(也就是目標容器的起點),在上例中指定為 dest.begin(),也就是 dest 的起點,如果找到符合的元素,就會將值複製至對應的位置,然後遞增迭代器,copy_if 執行過後會傳回迭代器(也就是已迭代至目標容器的哪個位置),為了要支援這樣的協定,目標容器必須有足夠的容量。
為什麼要這麼麻煩呢?因為 copy_if 是基於迭代器協定,而不是為了 vector 而設計,如果不想事先決定目標容器的容量,可以使用 iterator 的 back_inserter,這會包裹目標容器,目標容器必須支援 push_back 方法(例如 vector),傳回的迭代器若被用來指定值至對應位置時,底層會呼叫 push_back 方法在目標容器新增元素:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;
int main() {
vector<int> number = { 11, 12, 21, 30, 31, 41, 55, 66, 80, 98 };
vector<int> dest;
copy_if( number.begin(), number.end(), back_inserter( dest ), []( int n ) { return n % 2; } );
for_each( dest.begin(), dest.end(), []( int n ) { cout << n << " "; } ); // 11 21 31 41 55
return 0;
}
如果 filter 出來的值想從原容器去除,可以使用 remove_if,例如:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;
int main() {
vector<int> number = { 11, 12, 21, 30, 31, 41, 55, 66, 80, 98 };
auto removeStart = remove_if( number.begin(), number.end(), []( int n ) { return n % 2; } );
// 12 30 66 80 98
for_each( number.begin(), removeStart, []( int n ) { cout << n << " "; } );
cout << endl;
// 12 30 66 80 98 41 55 66 80 98
for_each( number.begin(), number.end(), []( int n ) { cout << n << " "; } );
return 0;
}
實際上,remove_if 並不是真的把元素從原容器去除了,它只是將要去除的元素往後移,然後回傳這些元素的起點,如果這些元素你真的不要了,可以使用 vector 的 erase 方法。例如:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;
int main() {
vector<int> number = { 11, 12, 21, 30, 31, 41, 55, 66, 80, 98 };
auto removeStart = remove_if( number.begin(), number.end(), []( int n ) { return n % 2; } );
number.erase( removeStart, number.end() );
// 12 30 66 80 98
for_each( number.begin(), removeStart, []( int n ) { cout << n << " "; } );
return 0;
}
至於具備一級函式的語言中愛談的 map,在 C++ 中可以使用 tranform 來解決,使用上跟 copy_if 類似,需要指定一個目標容器。例如:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;
int main() {
vector<int> number = { 11, 12, 21, 30, 31, 41, 55, 66, 80, 98 };
vector<int> dest;
transform( number.begin(), number.end(), back_inserter( dest ), []( int n ) { return n * 10; } );
// 110 120 210 300 310 410 550 660 800 980
for_each( dest.begin(), dest.end(), []( int n ) { cout << n << " "; } );
return 0;
}
雖然以上都是傳遞 lambda 運算式,實際上它們也可以接受函式位址。
再來,函式也可以傳回函式,這邊的指的函式傳遞,包括了函式指標、lambda 運算式。
從函式中傳回函式指標,基本上沒什麼問題,因為函式指標不會消失,然而,從函式中傳回 lambda 運算式,就得留意一下了,因為函式中的 lambda 運算式,生命周期就是侷限於函式之中,如果如下傳回函式:
#include <iostream>
using namespace std;
auto foo() {
auto f = [] { cout << "foo" << endl; };
return f;
}
int main() {
auto fn = foo();
fn(); //印出 foo
return 0;
}
那麼沒什麼問題,f 會複製給 fn,然而如果是傳回參考:
auto& foo() {
auto f = [] { cout << "foo" << endl; };
return f;
}
因為 foo 函式執行過後,呼叫者參考的 f 變數已經無效,編譯時就會產生警訊:
warning: reference to local variable 'f'
另一個問題是,若以參考方式捕捉了區域變數:
#include <iostream>
using namespace std;
auto foo() {
string text = "foo";
auto f = [&] { cout << text << endl; };
return f;
}
int main() {
auto fn = foo();
fn(); //印出 崏a (亂碼)
return 0;
}
編譯雖然會過,然而實際上捕捉的變數在 foo 函式執行過後已經無效,最後呼叫傳回的 lambda 運算式時,就會發生不可預期的結果,如果你是從其他具有一級函式特性的語言來到 C++,要記得的就是,C++ 的 lambda 運算式,並不會擴展被捕捉變數的生命周期。
這並不是指傳回 lambda 運算式時,就不能用 & 來捕捉變數,主要還是要看捕捉的變數,其位址是否有效,例如以下就沒有問題,因為實際上 lambda 運算式捕捉的變數,參考的位址是 main 中的 text 變數位址:
#include <iostream>
#include <string>
using namespace std;
auto foo( string &text ) {
auto f = [&] { cout << text << endl; };
return f;
}
int main() {
string text = "foo";
auto f = foo( text );
f();
return 0;
}
來看看傳回 lambda 運算式的一個例子,感覺像是函式產生了新函式,記憶了指定的引數:
#include <iostream>
using namespace std;
auto add( int m ) {
return [m]( int n ) { return m + n; };
}
int main() {
auto plus10 = add( 10 );
cout << plus10( 20 ) << endl; // 30
cout << plus10( 40 ) << endl; // 50
system( "pause" );
return 0;
}
那麼可不可以接受函式、傳回函式呢?
#include <iostream>
using namespace std;
int binary_fun(int, int);
int add(int m, int n) {
return m + n;
}
int mul(int m, int n) {
return m * n;
}
auto bind(decltype(binary_fun) bf, int fst) {
return [bf, fst] (int snd) { return bf(fst, snd); };
}
int main() {
auto add10 = bind(add, 10);
auto mul5 = bind(mul, 5);
cout << add10(30) << endl; // 40
cout << mul5(20) << endl; // 100
return 0;
}
上面範例的 bind 可以接受函式並回傳函式,回傳的函式綁定了第一個引數,像 bind 這類可以接受函式、回傳函式的函式,稱為高階函式(high-order function)。
實際上,functional 標頭檔就提供了個 bind 可以使用,而且更有彈性,可以指定要綁定哪個參數:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
int add(int m, int n) {
return m + n;
}
int mul(int m, int n) {
return m * n;
}
int main() {
auto add10 = bind(add, _1, 10);
auto mul5 = bind(mul, _1, 5);
cout << add10(30) << endl; // 40
cout << mul5(20) << endl; // 100
return 0;
}
佔位符 _1 是位於 std::placeholders 名稱空間之中,代表傳回的函式可接受的第一個參數,以上例來說,bind(add, _1, 10) 表示 add 的 a 會是佔位符 _1,b 會是 10,因此傳回的函式第一個參數接受到的引數,相當於指定了 a 的值。
因此若有多個參數要綁定,會是如下:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int a, int b, int c, int d) {
cout << "a: " << a << endl
<< "b: " << b << endl
<< "c: " << c << endl
<< "d: " << d << endl;
}
int main() {
auto wat = bind(foo, _1, 20, _2, 40);
wat(10, 30);
return 0;
}
在上例中,b 被綁定為 30,d 被綁定為 40,傳回的函式第一個引數值會是 a 的值,第二個引數值會是 c 的值,因此結果顯示如下:
a: 10
b: 20
c: 30
d: 40
因此,如果想調換參數順序,可以如下:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int a, int b) {
cout << "a: " << a << endl
<< "b: " << b << endl;
}
int main() {
auto wat = bind(foo, _2, _1);
wat(10, 20);
return 0;
}
輸出:
a: 20
b: 10
你可能會覺得上面這些範例有點多此一舉,但那些只是為了讓大家了解原理而舉出的例子。 實際上,functional 中包含了對應於運算子的函子(Functor),像是 plus、minus、multiplies 等,之後談 Class 時會談到函子,在這邊只要先知道,它就是個 Class ,重載了呼叫運算子 (),建構它的實例之後,可以看成是個函式。
因此,上例可以進一步修改如下:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
int main() {
auto add10 = bind(plus<int>{}, _1, 10);
auto mul5 = bind(multiplies<int>{}, _1, 5);
cout << add10(30) << endl; // 40
cout << mul5(20) << endl; // 100
return 0;
}
bind 預設不處理參考,因此若是以下的範例:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int &a, const int &b) {
a++;
cout << &b << endl;
}
int main() {
int a = 10;
int b = 20;
auto wat = bind(foo, a, b);
wat();
cout << "a: " << a << endl
<< "b: " << &b << endl;
return 0;
}
輸出:
0x61feb0
a: 10
b: 0x61feb8
可以看到 main 中的 a 值依舊是 10,而 foo 的 b 位址與 main 的 b 不同
若要符合參數的參考指定,可以使用 ref 與 cref,後者的 c 代表了 const,例如:
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
void foo(int &a, const int &b) {
a++;
cout << &b << endl;
}
int main() {
int a = 10;
int b = 20;
auto wat = bind(foo, ref(a), cref(b));
wat();
cout << "a: " << a << endl
<< "b: " << &b << endl;
return 0;
}
輸出:
0x61feb0
a: 11
b: 0x61feb0
可以看到 main 中的 a 值是 11,而 foo 的 b 位址與 main 的 b 相同