STL Vector/Map 的使用練習, 附 Ruby 對照程式碼
Tags: C++ STL
最近在練習使用 C++ STL 中的 Container 功能。嗯,寫著寫著,覺得很不順手啊。例如不能用 Vector/Set/Map 直接建表。Stack 的 pop() 方法沒有回傳值;我用 C 寫的 stack 功能, pop() 是會推一個值出來的。
挑了兩個 STL Container 的練習程式碼,再用 Ruby 寫一段相同的。兩相比較,也算在吐槽吧。
Vector 練習
把字元陣列的內容放入 Vector 容器 vec 中。將 vec 中的每個字元都轉換成大寫字母,最後倒著顯示。
輸出結果如下:
H
e
l
l
o
O
L
L
E
H
C++: 21-3.cc
不接受 vector vec("Hello") 的寫法。每次要建立一個容器時,總少不了重覆的存入動作程式碼。
Ruby: 21-3.rb
Map 練習
用 Map 建立一個顏色名稱與 RGB 代碼的對照表。輸出結果如下列:
blue => #0000ff
red => #ff0000
green => #00ff00
black => #ffffff
white => #000
'blue' Find! It's value is #0000ff
C++: 22-2.cc
原本用 C 字串陣列,後來改用 String 陣列。然而這個陣列 ss 只是「初始值」,還要重覆將這些初始值存入 Map 容器 myMap 中的程式碼。
Ruby: 22-2.rb
Ruby 可以直接建立對照表,不需要另外建立容器。
Posted by shirock at
樂多Roodo! │16:11
│
回應(13)
│
引用(0)
│
C/C++
引用URL
http://cgi.blog.roodo.com/trackback/3679815
C++ 和 ruby 設計理念的不同, 所以程式寫起來也不會一樣, 不過我很好奇的是, 像第一個例子明明就應該是用 std::string 為什麼一定要用 vector (vec 是 std::string, vec.split('') 之後才算是 std::vector), 當然要做也是可以, 我自己改的程式如下:
-------------
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
static void make_upper(char& c)
{
c = (c >= 'a' && c <= 'z')? (c - 0x20): c;
}
static void print_char(const char& c)
{
std::cout << c << "\n";
}
int main()
{
std::string ps("Hello");
std::vector<char> vec(ps.begin(), ps.end());
//上面兩行在這例子中也可以直接換成下面這個
//std::string vec("Hello");
for_each(vec.begin(), vec.end(), print_char);
for_each(vec.begin(), vec.end(), make_upper);
for_each(vec.rbegin(), vec.rend(), print_char);
return 0;
}
至於第二個, 因為語法的限制, C++ 沒定義也無法做這種神奇的"擴充"(就算是 macro 也做不到), 所以得乖乖的一個一個加進去, 不過 map 可以使用 map[key] = value 的方式來設定值和 map[key] 來取值, 所以上例我會改成這樣子:
---------------------
#include <iostream>
#include <iomanip>
#include <map>
#include <algorithm>
#include <string>
static void print_map(const std::map<std::string, std::string>::value_type& data)
{
//printf("%-10s => %-10s\n", data.first.c_str(), data.second.c_str());
std::cout << std::setiosflags(std::ios::left) << std::setw(10) << data.first
<< " => " << std::setw(7) << data.second << std::endl;
}
int main()
{
char* ss[] = {
"black", "#ffffff",
"white", "#000",
"blue", "#0000ff",
"red", "#ff0000",
"green", "#00ff00",
NULL
};
std::map<std::string, std::string> myMap;
for(char **q = ss; *q; q += 2)
myMap[*q] = *(q+1);
for_each(myMap.begin(), myMap.end(), print_map);
//這地方必需要用 find, 要不然會多建一個 myMap["blue"], 如果 blue 不存在的話
if(myMap.find("blue") != myMap.end())
std::cout << "'blue' Find! It's value is " << myMap["blue"] << std::endl;
}
我是覺得在這兩個語言的設計理念就有差別, 所以在 C++ 裡面必需要用一些比較仔細(或者是囉哩吧嗦)的方式來處理, 好處是可以想的更仔細一點, 壞處也是要想的很仔細(像什麼型態處理等等, 一個錯就各個錯), 而 ruby(perl, php 等等)裡就可以用一些語言上先天的優勢來簡化, 但壞處就有可能是會有地方想不周延或是有一些誤解的可能性, 各有各的好, 各有各的差. 對我來說, 能夠依情況找到適合自己用的語言就好了, 我也不會想要去計較哪一個比較麻煩哪一個比較簡單 :p
我想第一個例子可以寫的更STL
string s("hello");
vector<char> vec(s.begin(), s.end());
copy(vec.begin(), vec.end(), ostream_iterator<char>(cout, "\n"));
transform(vec.rbegin(), vec.rend(), ostream_iterator<char>(cout, "\n"), ptr_fun(::toupper));
第二個例子可以這樣做
map<string, string> clr_tbl; //註一
clr_tbl["black"] = "#ffffff";
clr_tbl["white"] = "#000000";
clr_tbl["blue"] = "#0000ff";
clr_tbl["red"] = "#ff0000";
clr_tbl["green"] = "#00ff00";
transform(clr_tbl.begin(), clr_tbl.end(), ostream_iterator<string>(cout, "\n"), select2nd<map<string, string>::value_type>()); //註二
if(clr_tbl.find("blue") != clr_tbl.end()) cout << "blue is " << clr_tbl["blue"] << endl;
註一
像這樣也可以啊
pair<string, string> tbl[] = { make_pair("black", "#ffffff"), make_pair("white", "#000000") };
map<string, string> clr_tbl(tbl, tbl + 2);
註二
假如不是用SGI STL的話很可能沒有他的select2nd, 但是可以自己仿造一個
template <typename T1, typename T2> T2 simu_sgi_select2nd(pair<T1, T2> const & tmp)
{
return tmp.second;
}
transform(clr_tbl.begin(), clr_tbl.end(), ostream_iterator<string>(cout, "\n"), ptr_fun(simu_sgi_select2nd<string, string>));
註三
其實拿STL map來跟純粹的table來比並不太適合
map是sorted的,如果Ruby的這樣一個table沒有sorted(如果啦,目前還沒時間研究Ruby)
那麼我會覺得直接拿vector<pair<string, string>> or list<pair<string, string>>甚至
直接string[][]配合STL的algorithms來做比較會更適合
老實說寫這短短幾行東西還搞蠻久的(下班還賴著不走吹冷氣是怎樣......)
因為我跟STL還不熟
那就是他接受度不高的原因(你得先跟它裝熟)
但是一旦了解它的觀念(literally, Concept, 就是"Generic Programming and the STL"這本看懂的意思啦)
就會發現用它真的跟組裝樂高很像, 而STL定義軟體元件的方式真讓人大開眼界......
STL和template可以達到generic
指的不單針對c++ built-in type而是還有自訂型別可以共用所有組件
STL有的不只是效能高的container還有其他組件iterator, function object, algorithm, adaptor......
又可以擴充或抽換它們
從這些面向定義軟體元件
我還沒有在其他的地方看過
我不是了解很多其他的語言
動態語言用一個general object來承接object
在runtime的時候再去依照物件(late binding)的實際支援method與否來決定結果(runtime error?)
仍然是polymorphism的味道較重
並不算是generic
至少不算是靜態語言中的generic
我覺得STL(C++ template)比較像是不失去靜態語言優點(solid)為前提追求靜態語言中最大的彈性
但是這個是動態語言和靜態語言基本的特性不同, 無關優劣
另外pop() 方法沒有回傳值這碼子事
算是STL兼顧效能和彈性的一點,十分合理喔
"Generic Programming and the STL"的9.2.3 Back Insertion Sequence有講到
我本來也是用 copy, 不過要硬要和 ruby 對照的話, 就全改為 for_each 了
不過 cf 的第二個例子輸出是沒有合要求的哦(當然, 寫一個 function 來做是最簡單的) :p
關於第一個例子為何用 vector ?
理由很簡單,因為 "Hello" 是一個「陣列」。
一個陣列,我們可以直接建表:
int ar = {1,2,3,4,5};
一個 vector,頂多只能指定每個元素的初值。
vector<int> vec(5,0);
然後重複:
vec[0] = 1;
vec[1] = 2;
...
cf 所提供的第二個例子也一樣:
map<string, string> clr_tbl;
clr_tbl["black"] = "#ffffff";
clr_tbl["white"] = "#000000";
這是一個令我覺得煩人的程式碼重複動作。
至於鍵值的排序,這點倒不在我的需求中。所以用 vector + pair 也成。但 vector 沒有 find() ,所以還要再給它一個 find 的 Algorithms 。但這牽涉到搜尋的效率問題。
Ruby 中的 table ,鍵值不排序,但搜尋是用雜湊法。考量搜尋與插入效率,在 STL 中,最接近的就是 Map 。List的話,少了個鍵值雜湊表,搜尋效率力有未逮。
關於 pop()。在 C 語言中,我實作的方式是給一個參數,如 pop(void*elm)。如果引數為 NULL ,直接丟棄;否則存入並回傳。這種寫法在 STL 中被視為不安全的。如果要回傳,那麼還要考慮複製或參照等事,確實沒效率。
其實,我用的例子應該算敲在 STL 的弱項上。對老練的 C 程序員而言,不用 STL 反而寫起來更簡單。就像 cf 說「直接string[][]配合STL的algorithms來做比較會更適合」。
關於polymorphism與generic。
從語法與語意看,動態語言的語法其實就是泛型的。它們只是因為沒有編譯動作,而不得不 late binding 。並不得不仰賴其他的處理策略。參考《
Use C/C++ with Dynamic Languages is Easier Than Pure C++》及《
個體之間協議互動行為的多種形式》。
我也可以換另一種說法,若從泛型與編譯器的關係來看,沒有編譯器的動態語言環境不需要像 template 這類的泛型功能。See also:
Ruby versus Smalltalk versus .... 所以 cf 說「我還沒有在其他的地方看過」是很合理的,因為 Ruby, Perl, JavaScript 等動態語言中用不著。
我不知道為什麼一定要把 C 的觀念硬套在 STL 上, STL 裡沒有陣列這種東西, 比較接近的是 Random Access Container 這類的 Container, 依照要求的話, 我會選用 std::basic_string<char> 而不是 std::vector<char>(這兩者都是 Random Access Container), 再來對於 "Hello" 來說, 它可以看做是一個陣列, 但是也是可以看做一個字串, 既然是字串的話, 當然用 std::basic_string<char> 下去處理會是最好的選擇
而第二個例子, 要達到 key => value 這個要求, 對映到 STL 上的就是 Pair Associative Container, 底下只有 map/multimap 可以選(在 SGI STL 裡, 還有 hash_map 和 hash_multimap 可用), 所以當然 map 是首選
至於不能很簡單的設值, 就只是語法的問題, 因為 C++ 先天的限制, 不可能很簡單的做到像是 std::vector<char> tmp = { 1, 2, 3 }; 這種事, 要嘛就是建立出來一個慢慢設, 要嘛就是先生出一個有初始值的 char[], 然後透過 std::vector(InputIterator begin, InputIterator end) 來建立, 那這是不是算弱項, 可能吧, 因為要多寫一堆東西, 看起來就不好看
至於 pop 的問題, 如果真的需要回傳的話, 也可以自己寫一個 function 來做相同的事:
template<class C> C::value_type pop(C& c) {
C::value_type tmp = c.back();
c.pop_back();
return tmp;
}
也不是不行啊, 不可能每一個函式庫都能滿足所有的需求的
maniac said: '對於 "Hello" 來說, 它可以看做是一個陣列, 但是也是可以看做一個字串.'
雖然我第一個例子用的是 "Hello" ,但我原本就是要練習用容器建表、查表和走訪,而不是練習字串處理。如果我用 string 就沒有這個練習效果了。
關於直接建表的事。在我接觸 STL 之前,我原本習慣用陣列建表,然後直接把陣列作為建構子的初始值。以前的做法,類似用陣列配 Algorithms 實作一個 class 。而不是先建構容器,再一一指派。所以一開始用 STL 的 Container 時,我對它不能直接用陣列作初始值這點感到不可思議。我一直以為它可以這麼做。
std::basic_string 也是一個容器, 和 std::vector 同樣都是有著 Random Access Container, Sequence 的容器, 通常我是視為同一類型的東西來處理, 所以我在我自己的應用上, 會選擇符合我需要特性的來處理, 像
char *tmp_string = "Hello";
std::vector<char> vec(tmp_string, tmp_string + strlen(tmp_strlen));
和
std::basic_string<char> vec("Hello");
兩個比起來, 我會選擇用後者的方式來建立, 而且這兩個處理方式也很類似, 不用因為操作的對象是 vector 或是 string 而有太大的差別(除了用到 string 特有的方式例外)
其實, 在給的 ruby 範例裡, 也是必需把 string 切割成 array(string) 才能對每一個字元做處理(就我所用過的 script 語言裡, 只有 php 可以直接對字串取出一個字元的動作: $str[$pos]), 比擬成用 STL 的話, 就是先從字串生成暫時的 std::vector<char> 物件, 再使用 for_each 方法對每一個 vector 所包含的 char 做處理, 這兩種本質其實是相去不遠的
至於不能用陣列作初始值? 當然那只是語法的問題, C++ 沒有方法可以用"匿名"陣列做出來類似 vector<char> vec = "Hello"; 或是 vector<int> vec = { 1, 2, 3 }; 這種東西, 所以 STL 也不可能超越這種限制, 但是如果是從已經有值的"具名"陣列來建立的話, 可以參考
http://www.sgi.com/tech/stl/Vector.html, std::vector 有一個方法是 template <class InputIterator> vector(InputIterator, InputIterator) [Creates a vector with a copy of a range.]
這個就是說, 我可以給一個陣列的起始值和它的終點來建立一個新的 vector, 比方說
int initial_array[3] = { 1, 2, 3 };
std::vector<int> vec(initial_array, initial_array + 3);
來做出內容是 { 1, 2, 3 } 的 vector, 至於不能用"匿名"陣列來做是好是壞, 對我來說就只是麻煩一點要多寫一些東西而已, 倒也沒什麼
類推,我的例子可以寫成:
char* ps = "Hello";
std::vector vec2(ps, ps+strlen(ps));
嗯,用指標做為容器建構初始值的寫法,應該是最直接的方式了。如果要讓容器建表像陣列一樣,那就要在語法上做修改了。算了,C++沒有必要這麼做。
Ruby 也可以用 str[pos] 的方式取出一個字元(char)。只是別忘了, char 的意義就是一個整數,所以 Ruby 的 str[pos] 之值是一個整數,而不是字串(string)。