C ++のmutableキーワード

mutableキーワードは、C ++言語のあまり知られていないコーナーを指します。 同時に、コードのconst-correctnessを厳密に遵守したい場合や、状態を変更できるラムダ関数を記述したい場合は、非常に便利です。



数日前、エリック・スモリコウスキはツイートしました:



「インタビューでは、プログラマーがC ++をどれだけよく知っているか(10段階で)尋ねます。 通常、彼らは8または9と答えます。そして、「可変」とは何かを尋ねます。 彼らは知りません。 :) "



このような質問と回答に対する私の印象は2つあります。 一方で、インタビューでそのような質問をすることは無益な問題であり、インタビュー対象者の能力についてはほとんど何も述べていません。 しかし、一方で、 mutableキーワードは多くのプログラマーにとって当然のこととして忘れられており、一部のシナリオでは非常に便利です。



定数の正確性:意味的不変性と構文的不変性



恒常性の概念を使用するという点で正しいコードを作成しようとすると、セマンティック不変性は構文不変性と同等ではないという事実に遭遇します。 言い換えれば、オブジェクトの状態を変更する必要がある場合があります(実装の詳細が必要な場合)が、オブジェクトの状態を外部から見えるように一定に保ちます。



深い技術的な理由から内部状態の変更が必要になる場合がありますが、これはクラスの外部クライアントには気付かないはずです。 しかし、私たちの選択は大きくありません-メソッドを宣言するときにconstキーワードを使用すると、クラス外の誰もこれらの変更に気付かない場合でも、コンパイラはこのクラスのオブジェクトを変更することを許可しません。



キャッシュデータ



良い例は、データキャッシングです。 このポリゴンクラスを見てみましょう。



class Polygon { std::vector<Vertex> vertices; public: Polygon(std::vector<Vertex> vxs = {}) : vertices(std::move(vxs)) {} double area() const { return geometry::calculateArea(vertices); } void add(Vertex const& vertex) { vertices.push_back(vertex); } //... };
      
      







geometry :: CalculateAreaは非常にリソースを消費する関数であり、area()メソッドが呼び出されるたびに呼び出す必要はないと想定しましょう。 ポリゴンを変更するときに新しい面積を計算できますが、シナリオによっては、それだけ(またはそれ以上)のリソースを消費する場合があります。 この状況での良い解決策は、必要な場合にのみ面積を計算し、結果をキャッシュし、ポリゴンが変更された場合にキャッシュをクリアすることです。



 class Polygon { std::vector<Vertex> vertices; double cachedArea{0}; public: //... double area() const { if (cachedArea == 0) { cachedArea = geometry::calculateArea(vertices); } return cachedArea; } void resetCache() { cachedArea = 0; } void add(Vertex const& vertex) { resetCache(); vertices.push_back(vertex); } //... };
      
      





しかし、ちょっと、ちょっと、それほど速くない! area()メソッドは定数としてマークされており、何らかの理由でその中のcachedAreaプロパティを変更しようとしているため、コンパイラはこのようなトリックを上げさせません。 メソッド宣言からconstを削除しますか? しかし、このクラスのクライアントは私たちを理解しません。 結局、area()は単純なゲッターであり、この関数はクラス内の何も変更しないはずです。 それで、なぜ彼女の宣言にconstがないのですか?



ミューテックス



別の例は、ミューテックスを使用したスレッドセーフです。 上記の例の頂点コンテナはスレッドセーフではありません。 したがって、異なるストリームが同じポリゴンからのデータを共有するマルチスレッドアプリケーションでは、このデータへのアクセスが安全であることを確認する必要があります。



 class Polygon { std::vector<Vertex> vertices; std::mutex mutex; public: Polygon(std::vector<Vertex> vxs = {}) : vertices(std::move(vxs)) {} double area() const { std::scoped_lock lock{mutex}; return geometry::calculateArea(vertices); } void add(Vertex const& vertex) { std::scoped_lock lock{mutex}; vertices.push_back(vertex); } //... };
      
      





この場合、コンパイラはarea()メソッドについて不満を言い始めますが、これは一定であることが強く約束されていますが、それ自体(悪党です!)は、mutexの状態を変更するmutex :: lock()操作を実行しようとしています。 つまり、定数ミューテックスをロックすることはできません。



エリア()メソッドを一定にすることはできず、スレッドの安全性を放棄するか、クラスのクライアントを誤解させ、メソッド宣言のconstを削除することが強制されることがわかりました。 実装の技術的な詳細は、外部から見えるオブジェクトの状態とはまったく関係がないため、機能の一部を拒否するか、クラスユーザーを誤解させる必要があります。



救助のためのキーワード「可変」



mutableキーワードは、このクラスの問題を解決するためのC ++言語標準に特に存在します。 クラスメンバ変数に追加して、特定の変数が定数コンテキストであっても変更できることを示すことができます。 可変を使用すると、上記の両方の例に対するソリューションは次のようになります。



 class Polygon { std::vector<Vertex> vertices; mutable double cachedArea{0}; mutable std::mutex mutex; public: //... double area() const { auto area = cachedArea; if (area == 0) { std::scoped_lock lock{mutex}; area = geometry::calculateArea(vertices); cachedArea = area; } return area; } void resetCache() { assert(!mutex.try_lock()); cachedArea = 0; } void add(Vertex const& vertex) { std::scoped_lock lock{mutex}; resetCache(); vertices.push_back(vertex); } //... };
      
      





可変ラムダ関数



mutableキーワードを使用する別のオプションがあり、ラムダ関数の状態保存に関連付けられています。 通常、クロージャー関数呼び出しステートメントは定数です。 言い換えると、ラムダは値によってキャプチャされた変数を変更できません。



 int main() { int i = 2; auto ok = [&i](){ ++i; }; //OK, i    auto err = [i](){ ++i; }; //:     i auto err2 = [x{22}](){ ++x; }; //:     x }
      
      





ただし、 mutableキーワードはラムダ関数全体に適用でき、これによりすべての変数が可変になります。



 int main() { int i = 2; auto ok = [i, x{22}]() mutable { i++; x+=i; }; }
      
      





クラス宣言の可変変数とは異なり、可変ラムダ関数は比較的まれに、非常に慎重に使用する必要があることに注意してください。 ラムダ関数の呼び出し間で状態を保存することは、危険で直観に反する可能性があります。



結論



mutableは、C ++言語の暗くて埃っぽいコーナーではありません。 これは、クリーンなコードで役割を果たすツールであり、constをより頻繁に使用するほど、またコードをより安全で信頼できるものにしようとするほど、より適切に機能します。 mutableを使用すると、コンパイラーに対して、そのチェックが重要かつ必要な場所、および回避したい場所をよりよく説明できます。 これにより、コードの全体的な正確さが向上します。



All Articles