(原創)C++11改進我們的程序之簡化我們的程序(一)


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>是編譯不過的,這里就要說說如何獲取閉包的返回值類型了。
獲取閉包的返回值類型的方法有三種:

  1. 通過decltype
  2. 通過declval
  3. 通過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函數吧:

 #include <map>
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 typename std::result_of<Fn(value_type)>::type ketype;
typedef  decltype(std::declval<Fn>()(std::declval <value_type>())) ketype;
//ty pedef decltype(f(value_type())) ketype;
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> 
groupby(const KeyFn& fnk, const ValueFn& fnv)  
{
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,歡迎大家來交流技術。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM