この記事はAltplus Advent Calendar 2017の6日目のエントリです。
こんにちは、職業Scalalian、C++愛好家の竹田です。
識別子は整数型で表現すること多いですが、整数型などをそのまま使用してしまうと、”順序付け可能である”や”配列のインデックスで使用できる”などの整数型の性質を前提にコーディングしてしまい危険です。
例えば、”順番通り”に並んでいて、ID番の要素にそのIDと関連付けられているデータを使用してコーディングをしてしまうと、様々なバグの温床になります。
#include <vector> #include <iostream> using ID = int; using Probability = double; using Image = int; static std::vector<Probability> probabilityForEachID = {0.1, 0.2, 0.3, 0.4}; static std::vector<Image> imageForEachID = {10, 20, 30, 40}; Probability badProbability(ID id){ return probabilityForEachID[id]; } Image badImage(ID id) { return imageForEachID[id]; } void displayImageAndProbability(Probability const& p, Image const& i){ std::cout << "probability = " << p << " , image = " << i << std::endl; } int main(){ displayImageAndProbability(badProbability(0), badImage(0)); }
このコードでは、probabilityForEachID
とimageForEachID
の、i番目の要素がID iに関連するデータでなければいけないという暗黙のルールを守っている間は、想定通りに動作します。しかし、データの追加やコード中の操作でprobabilityForEachID
とimageForEachID
の要素の並び順が変わってしまうと、インデックスとIDが対応しているという暗黙のルールが破られてしまい、IDに関連するデータが正確に引けなくなってしまいます。
識別子
問題の原点は、識別子に必要以上の性質を与えてしまったことにあります。Wikipediaで識別子の定義を引くと「識別子(しきべつし、英: identifier)とは、ある実体の集合の中で、特定の元を他の元から曖昧さ無く区別することを可能とする、その実体に関連する属性の集合のこと」(By Wikipedia)とあります。
識別子とは、集まりの中である個を他の個と区別するための情報です。"区別"をC++で表現するには、等価演算子が相応しいので、等価演算子をサポートして識別子を表現するクラスを導入して、どのようにコードが変わるか見てみます。
#include <unordered_map> #include <iostream> struct ID{ int id; }; bool operator==(ID lhs, ID rhs) { return lhs.id == rhs.id; } bool operator!=(ID lhs, ID rhs) { return !(lhs == rhs); } namespace std { // IDをunordered_mapで使用するための特殊化。 template<> struct hash<ID>{ size_t operator()(ID const& x) const { return hash<int>{}(x.id); } }; } using Probability = double; using Image = int; static std::unordered_map<ID, Probability> probabilityForEachID = {{ID{0}, 0.1}, {ID{2}, 0.3}, {ID{1}, 0.2}, {ID{3},0.4}}; static std::unordered_map<ID, Image> imageForEachID = {{ID{1}, 20}, {ID{2}, 30}, {ID{3}, 40}, {ID{0}, 10}}; Probability betterProbability(ID id){ return probabilityForEachID.at(id); } Image betterImage(ID id) { return imageForEachID.at(id); } void displayImageAndProbability(Probability const& p, Image const& i){ std::cout << "probability = " << p << " , image = " << i << std::endl; } int main(){ displayImageAndProbability(betterProbability(ID{0}), betterImage(ID{0})); }
このコードでは、IDの同値関係(operator==
)とHashを利用して、unordered_map
のキーにIDを利用しています。probabilityForEachID
とimageForEachID
のデータの順番をシャッフルしていますが、意図通りに動作します。
unordered_map
は、Hashマップで実装されています。メモリ使用量やメモリ・アクセス速度が気になる場合は、以下のようにstd::vector<std::pair<ID, Data>>
を使用することができます。ただしこの方法では、識別子の一意性などが保証できないため、同じIDに対するデータが複数ある状況になりえるので、データ用のコンテナを操作するところでは、事後条件のチェックやユニットテストを通じて、一意性を保証するようにしましょう。
またBoostが使える状況では、std::vector<std::pair>
のような性質でmap
のIFを提供しているflat_map
もあるので、flat_map
の使用も検討しましょう。
#include <vector> #include <iostream> #include <algorithm> struct ID{ int id; }; bool operator==(ID lhs, ID rhs) { return lhs.id == rhs.id; } bool operator!=(ID lhs, ID rhs) { return !(lhs == rhs); } using Probability = double; using Image = int; static std::vector<std::pair<ID, Probability>> probabilityForEachID = {{ID{0}, 0.1}, {ID{2}, 0.3}, {ID{1}, 0.2}, {ID{3},0.4}}; static std::vector<std::pair<ID, Image>> imageForEachID = {{ID{1}, 20}, {ID{2}, 30}, {ID{3}, 40}, {ID{0}, 10}}; Probability betterProbability(ID id){ auto const found = std::find_if(probabilityForEachID.begin(), probabilityForEachID.end(), [&](auto const& p){ return p.first == id; }); if(found == probabilityForEachID.end()){ throw std::out_of_range("Not found."); } return found->second; } Image betterImage(ID id) { auto const found = std::find_if(imageForEachID.begin(), imageForEachID.end(), [&](auto const& p){ return p.first == id; }); if(found == imageForEachID.end()){ throw std::out_of_range("Not found."); } return found->second; } void displayImageAndProbability(Probability const& p, Image const& i){ std::cout << "probability = " << p << " , image = " << i << std::endl; } int main(){ displayImageAndProbability(betterProbability(ID{0}), betterImage(ID{0})); }
最後に
あるコンセプト(識別子)の表現に必要以上の性質(int型で表現して、順序付けやインデックスとしての使用できる性質)を与えてしまうと、バグの温床になることを見てきました。
自分で型を定義して適切な性質を与えることで、安全で見通し良いコードを書くことができます。