讓我們假設有一個支援預設建構函式以及從一個double構造和賦值的Widget類:
class Widget {
public:
Widget();
Widget(double weight);
Widget& operator=(double weight);
...
}
現在讓我們假設我們想建立一個從int到Widget的map,而且我們想有初始化有特定值的映射。這可以簡化為:
map<int, Widget> m;
m[1] = 1.50;
m[2] = 3.67;
m[3] = 10.5;
m[4] = 45.8;
m[5] = 0.0003;
實際上,簡化掉的唯一事情是忘了實際上的進行了什麼操作。那很糟糕,因為實際進行的會造成一個相當大的效能衝擊。
map的operator[]函數是個奇怪的東西。它與vector、deque和string的operator[]函數無關,也和內建的數組operator[]無關。相反,map::operator[]被設計為簡化“添加或更新”功能。即,給定
map<K, V> m;
這個運算式
m[k] = v;
檢查鍵k是否已經在map裡。如果不,就添加上,以v作為它的對應值。如果k已經在map裡,它的關聯值被更新成v。
這項工作的原理是operator[]返回一個與k關聯的值對象的引用。然後v賦值給所引用(從operator[]返回的)的對象。當要更新一個
已存在的鍵的關聯值時很直接,因為已經有operator[]可以用來返回引用的值對象。但是如果k還不在map裡,operator[]就沒有可以引用
的值對象。那樣的話,它使用實值型別的預設建構函式從頭開始建立一個,然後operator[]返回這個建立立對象的引用。
讓我們再次地看看原先例子的第一部分:
map<int, Widget> m;
m[1] = 1.50;
運算式m[1]是m.operator[](1)的簡化,所以這是一個map::operator[]的調用。那個函數必須返回一個Widget的
引用,因為m
的映射類型是Widget。在這裡,m裡面還沒有任何東西,所以鍵1在map裡沒有入口。因此operator[]預設構造一個Widget來作為關聯到
1的值,然後返回到那個Widget的引用。最後,Widget成為賦值目標:被賦值的值是1.50。
換句話說,這個語句
m[1] = 1.50;
功能上等價於這個:
typedef map<int, Widget> IntWidgetMap; // 方便的
// typedef
pair<IntWidgetMap::iterator, bool> result = // 用鍵1建立新
m.insert(IntWidgetMap::value_type(1, Widget())); // 映射入口
// 和一個預設構造的
// 值對象;
// 看下面對於
// value_type的
// 注釋
result.first->second = 1.50; // 賦值給
// 新構造的
// 實值型別
現在已經很清楚為什麼這種方法可能降低效能了。我們先預設構造一個Widget,然後我們立即賦給它新值。如果用想要的值構造Widget比預設構
造Widget然後進行賦值顯然更高效,我們就應該用直截了當的insert調用來替換operator[]的使用(包括它的構造加賦值):
m.insert(IntWidgetMap::value_type(1, 1.50));
這與上面的那些代碼有相同的最終效果,除了它通常節省了三次函數調用:一個建立臨時的預設構造Widget對象,一個銷毀那個臨時的對象和一個對
Widget的賦值操作。那些函數調用越昂貴,你通過使用map-insert代替map::operator[]就能節省越多。
上面的代碼利用了每個標準容器都提供的value_type typedef。這typedef沒有什麼特別重要的,但對於map和multimap(以及非標準容器的hash_map和hash_multimap——參見條款25
),記住它是很重要的,容器元素的類型總是某種pair。
我早先談及的operator[]被設計為簡化“添加或更新”功能,而且現在我們理解了當“增加”被執行時,insert比operator[]更高效。當我們做更新時,情形正好相反,也就是,當一個等價的鍵(參見條款19
)這已經在map裡時。為了看出為什麼情況是這樣,看看我們的更新選項:
m[k] = v; // 使用operator[]
// 來把k的值
// 更新為v
m.insert(
IntWidgetMap::value_type(k, v)).first->second = v; // 使用insert
// 來把k的值
// 更新為v
文法本身也許會讓你信服地支援operator[],但在這裡我們關注於效率,所以我們將忽略它。insert的調用需要
IntWidgetMap::value_type類型的實參(即pair<int,
Widget>),所以當我們調用insert時,我們必須構造和析構一個那種類型的對象。那耗費了一對建構函式和解構函式,也會造成一個
Widget的構造和析構,因為pair<int,
Widget>本身包含了一個Widget對象,operator[]沒有使用pair對象,所以沒有構造和析構pair和Widget。
因此出於對效率的考慮,當給map添加一個元素時,我們斷定insert比operator[]好;而從效率和美學考慮,當更新已經在map裡的元
素值時operator[]更好。如果STL提供一個兩全其美的函數,即,在句法上迷人的包中的高效的“添加或更新”功能。例如,很容易可以想象一個像
這樣的調用介面:
iterator affectedPair = // 如果鍵k不再map m中;高效地
efficientAddOrUpdate(m, k, v); // 把pair(k, v)添加到m中;否則
// 高效地把和k關聯
// 的值更新為v。返回一個
// 指向添加或修改的
// pair的迭代器
但是,在STL內沒有像這樣的函數,正如下面的代碼所示範的,自己寫一個並不難。那些注釋總結了正在做什麼,而且隨後的段落也提供了一些附加的解釋。
template<typename MapType, // map的類型
typename KeyArgType, // KeyArgType和ValueArgtype
typename ValueArgtype> // 是型別參數
// 的原因請看下面
typename MapType::iterator
efficientAddOrUpdate(MapType& m,
const KeyArgType& k,
const ValueArgtype& v)
{
typename MapType::iterator Ib = // 找到k在或應該在哪裡;
m.lower_bound(k); // 為什麼這裡
// 需要“typename”
// 參見第7頁
if(Ib != m.end() && // 如果Ib指向一個pair
!(m.key_comp()(k, Ib->first))) { // 它的鍵等價於k...
Ib->second = v; // 更新這個pair的值
return Ib; // 並返回指向pair的
} // 迭代器
else{
typedef typename MapType::value_type MVT;
return m.insert(Ib, MVT(k, v)); // 把pair(k, v)添加到m並
} // 返回指向新map元素的
} // 迭代器
執行一個高效的增加或更新,我們需要能找出k的值是否在map中;如果是這樣,那它在哪裡;如果不是,它該被插入哪裡。這個工作是為low_bound(參見條款45
)量身定做的,所以在這裡我們調用那個函數。確定lower_bound是否用我們要尋找的鍵找到了一個元素,我們對後半部分進行一個等價測試(參見條款19
),一定要對map使用正確的比較函數:通過map::key_comp提供的比較函數。等價測試的結果告訴我們應該進行增加還是更新。
如果是更新,代碼很直截了當。插入分支更有趣,因為它使用了insert的“提示”形式。結構m.insert(Ib,
MVT(k,v))“提示”了Ib鑒別出了鍵等價於k的新元素正確的插入位置,而且標準保證如果提示正確,那麼插入將在分攤的常數時間內發生,而不是對數
時間。在efficientAddOrUpdate裡,我們知道Ib鑒別出了適當的插入位置,因此insert的調用被保證為是一次常數時間操作。
這個實現的一個有趣方面是KeyArgType和ValueArgType不必是儲存在map裡的類型。它們只需要可以轉換
到儲
存在map裡的類型。一個可選的方法是去掉型別參數KeyArgType和ValueArgType,改為使用MapType::key_type和
MapType::mapped_type。但是,如果我們那麼做,在調用時我們可能強迫發生不必要的類型轉換。例如,再次看看我們在本條款的例子裡使用
的map定義:
map<int, Widget> m; // 同前
別忘了Widget接受從一個double賦值:
class Widget { // 也同前
public:
...
Widget& operator=(double weight);
...
};
現在考慮efficientAddOrUpdate的調用:
efficientAddOrUpdate(m, 10, 1.5);
假設是一次更新操作,即,m已經包含鍵是10的元素。那樣的話,上面的模板推斷出ValueArgType是double,函數體直接把1.5作為
double賦給與10相關的那個Widget。那是通過調用Widget::operator(double)完成的。如果我們用了
MapType::mapped_type作為efficientAddOrUpdate的第3個參數的類型,在調用時我們得把1.5轉化成一個
Widget,那樣的話我們就得花費本來不需要的一次Widget構造(以及隨後的析構)。
efficientAddOrUpdate實現中的細節可能很有趣,但它們沒有本條款的要點重要,也就是當關乎效率時應該在
map::operator[]和map-insert之間仔細選擇。如果你要更新已存在的map元素,operator[]更好,但如果你要增加一個新
元素,insert則有優勢。