C++11:變長模板的迭代與遞歸擴展

迭代的運行效率始終強於遞歸,遞歸始終比迭代方便開發。

變長模板屬於C++11中比較複雜的技術,在此簡單介紹下。

#include <iostream>
using namespace std;

template<class... Args>
int Sum (Args... args) {
	return sizeof...(args);
}

int main (int argc, char* argv []) {
	cout << Sum (1,2,23,124,4,23,43,24,32,4,23) << endl;
	return 0;
}
這是一個最簡單的變長模板,首先是template定義,class後面加三個點就代表不固定長度。

接下來是Sum函數,在main函數的調用中實例化,被擴展爲如下形式:

int Sum(int,int,int,int,int,int,int,int,int,int,int)
傳了11個參數,就被實例化爲11個參數形式。然後是sizeof...,這個宏是用來獲取變長模板中元素的個數用的。我們調用時傳了11個參數,它就返回11。程序運行的結果也相應爲11。變長模板對於變長參數的優勢之一爲第一個參數不用定義參數個數(關於變長參數的訪問詳見深度研究C語言變長函數),但相應的劣勢爲無法直接訪問參數。文章後面將簡要介紹如何調用參數。

這是最簡單的用法,接下來看看稍微複雜點的。

將參數進行一一比較,如果不爲3,顯示“值 is not 3”,如果爲3則顯示“find 3”然後立即返回。首先我們用遞歸來實現:

#include <iostream>
using namespace std;

template<class T, class... Args>
bool check (T t, Args... args) {
	if (t == 3) {
		cout << "find 3" << endl;
	} else {
		cout << t << " is not 3" << endl;
	}
	return (t != 3) && check (args...);
}

template<class T>
bool check (T t) {
	if (t == 3) {
		cout << "find 3" << endl;
	} else {
		cout << t << " is not 3" << endl;
	}
	return (t != 3);
}

int main (int argc, char* argv []) {
	check (1,2,5,7,9,3,5,6);
	return 0;
}

程序執行結果如下:


由於模板長度不固定,所以我們一次取一個。也就是上面的check方法,對應兩個參數及以上的情況,然後寫了下面這個方法,用於對應一個參數的情況。
這個例子的代碼還算比較簡潔的。但變長模板展開後是什麼情況呢?就像下面這樣:

#include <iostream>
using namespace std;

bool check (int i1) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3);
}

bool check (int i1, int i2) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3) && check (i2);
}

bool check (int i1, int i2, int i3) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3) && check (i2, i3);
}

bool check (int i1, int i2, int i3, int i4) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3) && check (i2, i3, i4);
}

bool check (int i1, int i2, int i3, int i4, int i5) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3) && check (i2, i3, i4, i5);
}

bool check (int i1, int i2, int i3, int i4, int i5, int i6) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3) && check (i2, i3, i4, i5, i6);
}

bool check (int i1, int i2, int i3, int i4, int i5, int i6, int i7) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3) && check (i2, i3, i4, i5, i6, i7);
}

bool check (int i1, int i2, int i3, int i4, int i5, int i6, int i7, int i8) {
	if (i1 == 3) {
		cout << "find 3" << endl;
	} else {
		cout << i1 << " is not 3" << endl;
	}
	return (i1 != 3) && check (i2, i3, i4, i5, i6, i7, i8);
}

int main (int argc, char* argv []) {
	check (1,2,5,7,9,3,5,6);
	return 0;
}
乍一看,簡直嚇尿。完全是新手寫的代碼。事實就是這樣,所有通過遞歸實現的變長模板展開都是這樣。上面的例子調用8個參數的重載,然後它調用7個函數的重載,然後調用6個函數的重載……

效果是實現了,可是這代碼被擴展了這麼多次,太浪費了。有沒更好的實現方式呢?有,通過迭代。以下示例通過迭代展開變長模板:

#include <iostream>
#include <vector>
#include <functional>
using namespace std;

template<class... Args>
void fun (Args... args) {
	function<bool (int)> check = [] (int t) {
		if (t == 3) {
			cout << "find 3" << endl;
		} else {
			cout << t << " is not 3" << endl;
		}
		return t != 3;
	};
	vector<function<bool ()>> fun = { bind (check, args)... };
	for (auto f : fun) {
		if (!f ()) break;
	}
}

int main (int argc, char* argv []) {
	fun (1,2,5,7,9,3,5,6);
	return 0;
}
代碼執行結果與上面的遞歸的執行結果相同。邏輯貌似比上面更不易懂,我們依次來分析。首先是fun函數,由於不是一次一次的取,所以不用寫兩次重載,比迭代稍微方便些。然後是一個check函數lambda表達式,這個表達式只是爲了讓check函數只能在fun函數內訪問,減少公開不必要的接口。重點不在這,重點在下面一行,vector這行。這行使用了三個C++11專有特性。

第一個是bind,用於函數的地址與參數綁定。爲何需要綁定?因爲變長模板擴的迭代擴展首先就是初始化逗號表達式。如果這那行像下面這樣寫:

bool val[] = { check(args)... };
變長模板將會將其擴展爲如下形式:
bool val[] = { check(arg0), check(arg1), check(arg2), check(arg3), check(arg4), check(arg5), check(arg6), check(arg7) };
省略號將在擴展時一次性全部擴展,這樣導致的結果就是初始化時被全部調用,不能實現調用到3之後立即返回的功能。
而上面的std::bind函數恰好解決了這個問題,它可以將函數或仿函數與參數綁定,返回一個內部仿函數,這樣,調用這個內部仿函數,就能調用原始的函數並傳入綁定的參數了。

上面的代碼模板擴展之後長這樣:

#include <iostream>
#include <vector>
#include <functional>
using namespace std;

void fun (int i1, int i2, int i3, int i4, int i5, int i6, int i7, int i8) {
	function<bool (int)> check = [] (int t) {
		if (t == 3) {
			cout << "find 3" << endl;
		} else {
			cout << t << " is not 3" << endl;
		}
		return t != 3;
	};
	vector<function<bool ()>> fun = {
		bind (check, i1),
		bind (check, i2),
		bind (check, i3),
		bind (check, i4),
		bind (check, i5),
		bind (check, i6),
		bind (check, i7),
		bind (check, i8)
	};
	for (auto f : fun) {
		if (!f ()) break;
	}
}

int main (int argc, char* argv []) {
	fun (1,2,5,7,9,3,5,6);
	return 0;
}
這兒還涉及到一個問題,那就是C++11的初始化列表。能讓vector或自定義類實現{xxx,xxx}這樣的初始化。這樣就不用一個一個push_back了。
然後,接下來是基於範圍的for循環了。依次取每個綁定,由於綁定自帶參數,所以這兒就不用傳參了(注意vector的聲明,函數返回值爲bool但沒有參數)。如果bool返回假那麼退出循環,將不會調用後面的代碼。

這只是一個簡單的實現,可見迭代的代碼效率優勢在任何時候都是強於遞歸的。

最後說一個需要注意到的地方。變長模板擴展不是你想擴展就能擴展,比如擴展成這樣  check(0)&&check(1)&&check(2)……   貌似很好看,實際上你是想多了。一次性擴展還只能用逗號實現,否則上面代碼就不會拐個彎了。如果C++17能實現這樣的擴展就好玩了……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章