C++11在很多方面可以簡化我們的程序開發,我會在“簡化我們的程序”這一系列的博文中一一講到,敬請關注。這次要講的是:C++11如何通過獲取函數模板的返回值類型來簡化我們的程序。
在談到簡化之前,我們先看一個問題,這個問題也是我前段時間在開發C++版本的linq時遇到的。假設我們現在需要將集合按某種屬性分組,就是類似於sql語句中的group by,我們知道group by后面的字段會組成一個唯一的鍵,得到的結果是按照這個唯一鍵值的分組。關於group by具體看一個例子就清楚了。
struct Person { string name; int age; string city; }; vector<Person> vt = {{"aa", 20, "shanghai"},{"bb", 25, "beijing"},{"cc", 25, "nanjing"},{"dd", 20, "nanjing"}}; 如果我們按年齡分組的話,得到的結果就是 {20, {"aa", 20, "shanghai"}},{20, {"dd", 20, "nanjing"}},{25,{"bb", 25, "beijing"}}, {25,{"cc", 25, "nanjing"}},實際上最終結果就是兩組,第一組是鍵為20,值是名稱為"aa"和"dd"的兩個Person; 第二組是鍵為25,值是名稱為"bb"和"cc"的兩個Person。
通過這個例子,大家應該對group by的含義就清楚了,做起來也好做。
比較簡單的作法是遍歷vector中的Person,凡是相同年齡的就歸為一組,用multimap<int, Person>來存放分組,代碼可能是這樣的:
multimap<int, Person> GroupByAge(const vector<Person>& vt) { multimap<int, Person> map; std::for_each(vt.begin(), vt.end(), [&map](const Person& person) { map.insert(make_pair(person.age, person)); }); return map; }
寫完上面的代碼后再測試下發現沒問題,很簡單就搞定了。但還沒完,如果我要按名稱分組呢?第一反應就是, 不是一樣的嗎,很簡單,copy一下改下鍵值就OK了。
multimap<string, Person> GroupByName(const vector<Person>& vt) { multimap<string, Person> map; std::for_each(vt.begin(), vt.end(), [&map](const Person& person) { map.insert(make_pair(person.name, person)); }); return map; }
是的,很簡單就搞定了,但是大家有沒有發現這兩段代碼除了map的鍵值不同外,其它的都一模一樣,能簡化成一個函數嗎?能,通過模板搞定嘛。
template<typename T> multimap<T, Person> GroupBy(const vector<Person>& vt) { multimap<T, Person> map; std::for_each(vt.begin(), vt.end(), [&map](const Person& person) { map.insert(make_pair(person.name, person)); //不行了,這個地方不能選擇鍵值了 }); return map; }
當我們寫下上面的代碼時發現行不通了,因為map.insert(make_pair(person.name, person)); 這里的鍵值可能是變化的,它有可能是Person中的任意一個字段,
也可能是這些字段的任意組合。問題就就在這里,我們不能通過一個泛型的模板參數T去選擇鍵值的類型!這樣的話以后如果要根據城市分組的話是不是又要拷貝
一份代碼,而僅僅是改個鍵值。這種蛋疼的重復代碼很丑陋,重復代碼是萬惡之源,一定要消除!如何消除這種重復呢?我們先分析一下這幾段重復代碼的特征:僅僅是某個類型不同,其它行為是一樣的,
問題的關鍵就是如何把這些不同的類型統一起來!本質上就是如何將類型擦除,關於類型擦除,我前面的博文講到過,不知道的童鞋請點這里,c++中的類型擦除,
里面介紹了五種方式,在這里我打算用第五種方式,通過閉包去擦除類型,因為鍵值的選擇權在外面,應該開放給用戶去選擇。代碼可能是這樣:
template<typename T> multimap<T, Person> GroupBy(const vector<Person>& vt, const Fn& keySlector) { multimap<T, Person> map; std::for_each(vt.begin(), vt.end(), [&map](const Person& person) { map.insert(make_pair(keySlector(person), person)); //keySlector返回值就是鍵值,通過keySelector擦除了類型 }); return map; }
上面的代碼通過閉包來擦除了鍵值類型,至於到底是什么類型的鍵值,都是keySlector中決定,是age還是name或者是city都OK。
測試代碼:
void TestGroupBy() { vector<Person> vt{...}; //按年齡分組 GroupBy<int>(vt, [](const Person& person){return person.age;}); //按年齡分組 GroupBy<string>(vt, [](const Person& person){return person.name;}); //按年齡分組 GroupBy<string>(vt, [](const Person& person){return person.city;}); }
恩,終於通過類型擦除把邏輯都統一成一個函數了,簡化了N多重復代碼,看得也挺舒服的。不過沒完,是的,就是沒完,睜大眼睛再看看吧。
對於這段代碼,至於你們滿不滿意我不知道,我不滿意。我不滿意的地方有一個就是GroupBy<T>要帶一個類型,這個類型是鍵值類型,也是閉包keySelector
返回值的類型。為什么每次都要把這個類型帶上呢,我很懶,不想把它帶上,我覺得通過keySelector就可以推斷出返回值類型,完全沒必要帶着這個類型,除了
多敲幾個代碼之外沒有任何好處。是的,我就想這樣調用:
GroupBy(vt, [](const Person& person){return person.age;}); //不需要帶着閉包的返回值類型 GroupBy(vt, [](const Person& person){return person.name;}); GroupBy(vt, [](const Person& person){return person.city;});
要做到這樣,問題的關鍵是如何推斷出lamda表達式的返回值類型!C++11不是提供了推斷表達式類型decltype嗎,試試看如何推斷出lamda表達式的返回值類型吧。
template<typename Fn> multimap<T, Person> GroupBy(const vector<Person>& vt, const Fn& keySlector) { typedef decltype(keySlector(Person)) key_type; //推斷出keySlector的返回值類型 multimap<key_type, Person> map; std::for_each(vt.begin(), vt.end(), [&map](const Person& person) { map.insert(make_pair(keySlector(person), person)); }); return map; }
問題是函數返回值中的那個T如何搞定呢?這樣multimap<decltype(keySlector(Person)), Person>是編譯不過的,這里就要說說如何獲取閉包的返回值類型了。
獲取閉包的返回值類型的方法有三種:
- 通過decltype
- 通過declval
- 通過result_of
我們先看看第一種方式,通過decltype:
multimap<decltype(keySlector((Person&)nulltype)), Person>或者multimap<decltype(keySlector(*((Person*)0))), Person>
這種方式可以解決問題,但不夠好,因為它有兩個魔法數:nulltype和0,看得蛋疼,不知道它們是怎么跑出來的。再看看第二種方式吧:
通過declval:
multimap<decltype(declval(Fn)(declval(Person))), Person>
這種方式用到了declval,declval的強大之處在於它能獲取任何類型的右值引用,而不管它是不是有默認構造函數,因此我們通過declval(Fn)獲得了function的右值引用,
然后再調用形參declval(Person)的右值引用,需要注意的是declval獲取的右值引用不能用於求值,因此我們需要用decltype來推斷出最終的返回值。
這種方式比剛才那種方式要好一點,因為消除了魔法數,但是感覺稍微有點麻煩,寫的代碼有點繁瑣,有更好的方式嗎?看第三種方式吧:
通過result_of
multimap<typename std::result_of<Fn(Person)>::type, Person>
std::result_of<Fn(Arg)>::type可以獲取function的返回值,沒有魔法數,也沒有declval繁瑣的寫法,很優雅。其實,查看源碼就知道result_of內部就是通過declval實現的,作法和方式二一樣,只是簡化了寫法。
至此,我們解決了所有問題,看看最終的GroupBy函數吧:
template<typename R>
class Range
{
public:
typedef typename R::value_type value_type;
Range(R& range) : m_range(range)
{
}
~Range()
{
}
template<typename Fn>
multimap<typename std::result_of<Fn(value_type)>::type, value_type> groupby(const Fn& f) //decltype(f(*((value_type*)0))),f((value_type&)nullptr)
{
/

typedef decltype(std::declval<Fn>()(std::declval <value_type>())) ketype;
//ty

multimap<ketype, value_type> mymap;
std::for_each(begin(m_range), end(m_range), [&mymap, &f](value_type item)
{
mymap.insert(make_pair(f(item), item));
});
return mymap;
}
template<typename KeyFn, typename ValueFn>
multimap<typename std::result_of<KeyFn(value_type)>::type, typename std::result_of<ValueFn(value_type)>::type>
{
typedef typename std::result_of<KeyFn(value_type)>::type ketype;
typedef typename std::result_of<ValueFn(value_type)>::type valype;
multimap<ketype, valype> mymap;
std::for_each(begin(m_range), end(m_range), [&mymap, &fnk, &fnv](const value_type& item)
{
ketype key = fnk(item);
valype val = fnv(item);
mymap.insert(make_pair(key, val));
});
return mymap;
}
private:
R m_range;
};
有童鞋反應之前的代碼不通用,我只是舉個例子,做到通用比較簡單,上面的range中的grouby對任何集合都有效。測試代碼:
struct Person { string name; int age; string city; }; template<typename T, typename... R> void PrintMap(T t, const R&... range) { PrintMap(t); PrintMap(range...); } template<typename R> void PrintMap(const R& range) { for (auto it = std::begin(range); it != std::end(range); it++) { std::cout << it->first << " " << it->second.name << " " << it->second.age << " " << it->second.city << endl; } } template<class Tuple, std::size_t N> struct TuplePrinter { static void print(const Tuple& t) { TuplePrinter<Tuple, N - 1>::print(t); std::cout << ", " << std::get<N - 1>(t); } }; template<class Tuple> struct TuplePrinter<Tuple, 1>{ static void print(const Tuple& t) { std::cout << std::get<0>(t); } }; template<class... Args> void PrintTuple(const std::tuple<Args...>& t) { std::cout << "("; TuplePrinter<decltype(t), sizeof...(Args)>::print(t); std::cout << ")\n"; } void TestGroupBy() { vector<Person> vt = { {"aa", 20, "shanghai"}, { "bb", 25, "beijing" }, { "cc", 25, "nanjing" }, { "dd", 20, "nanjing" } }; Range <vector<Person>> range(vt); auto r1 = range.groupby([](const Person& person){return person.age; }); auto r2 = range.groupby([](const Person& person){return person.name; }); auto r3 = range.groupby([](const Person& person){return person.city; }); auto r4 = range.groupby([](const Person& person){return std::tie(person.name, person.age); }); auto r5 = range.groupby([](const Person& person){return std::tie(person.name, person.age); }, [](const Person& person){return std::tie(person.city); }); PrintMap(r1, r2, r3); for (auto it = std::begin(r5); it != std::end(r5); it++) { PrintTuple(it->first); PrintTuple(it->second); } }
測試結果:
恩,一個通用的、簡潔的GroupBy函數就這樣誕生了,沒有重復代碼、沒有魔法數、沒有多余的類型,簡潔而優雅,可以坐下喝杯茶再回味一下了...
c++11 boost技術交流群:296561497,歡迎大家來交流技術。