[CppCon 2018]ハーブサッター:よりシンプルで強力なC ++を目指して



CppCon 2018でのスピーチで、ハーブサッターは2つの方向で彼の業績を一般に発表しました。 まず第一に、それは変数の寿命 (Lifetime)の制御であり 、コンパイル段階でバグのクラス全体を検出することができます。 第二に、これはコードの重複を回避するメタクラスに関する更新された提案であり、一度クラスカテゴリの動作を記述してから1行で特定のクラスに接続します。







はじめに:より多く=簡単ですか?!



C ++の非難は、規格が無意味に容赦なく成長していると聞いています。 しかし、最も熱心な保守派でさえ、range-for(コレクションサイクル)やauto(少なくともイテレータの場合)などの新しい構成によってコードが簡単になるとは主張しません。 実際のコードを簡素化するために、新しい言語拡張(少なくとも1つ、理想的にはすべて)を満たす必要があるおおよその基準を作成できます。







  1. コードの削減、簡素化、重複コードの削除(range-for、auto、lambda、Metaclasses)
  2. 安全なコードを記述しやすくし、エラーや特殊なケース(スマートポインター、ライフタイム)を防ぎます
  3. 古い、機能性の低い機能を完全に置き換えます(typedef→using)


Herb Sutterは、「モダンC ++」-現代のコーディング標準( C ++ Core Guidelinesなど )に準拠する機能のサブセットを識別し、完全な標準を誰もが知る必要のない「互換モード」と見なします。 したがって、「モダンC ++」が大きくならない場合は、すべて問題ありません。







変数の有効期間の確認(有効期間)



新しいLifetime Verification Groupは、ClangおよびVisual C ++のコアガイドラインチェッカーの一部として利用できるようになりました。 目標は、Rustのように絶対的な厳密さと精度を達成することではなく、個々の機能内で簡単かつ迅速なチェックを実行することです。







検証の基本原則



寿命分析の観点から、タイプは3つのカテゴリに分類されます。









ポインターは、次のいずれかの状態になります。









ポインターと値



各ポインターについて 追跡されている -それが示す可能性のある値のセット。 値を削除するとき、その出現はすべて に置き換えられました 。 ポインター値にアクセスするとき そのような エラーを発行します。







 string_view s; // pset(s) = {null} { char a[100]; s = a; // pset(s) = {a} cout << s[0]; // OK } // pset(s) = {invalid} cout << s[0]; // ERROR: invalid ∈ pset(s)
      
      





注釈を使用して、値にアクセスする操作と見なされる操作を構成できます。 デフォルトでは、 *



->



[]



begin()



end()



です。







無効なインデックスにアクセスした時点でのみ警告が発行されることに注意してください。 値が削除されても、誰もこのポインターにアクセスしない場合、すべてが正常に動作しています。







道標と所有者



ポインターの場合 所有者に含まれる値を示します それからこれ







所有者を取得するメソッドと関数は、次のように分類されます。









古いコンテンツ所有者が発表されました 所有者の削除時または無効化操作の適用時。







これらのルールは、C ++コードの多くの典型的なバグを検出するのに十分です。







 string_view s; // pset(s) = {null} string name = "foo"; s = name; // pset(s) = {name'} cout << s[0]; // OK name = "bar"; // pset(s) = {invalid} cout << s[0]; // ERROR
      
      





 vector<int> v = get_ints(); int* p = &v[5]; // pset(p) = {v'} v.push_back(42); // pset(p) = {invalid} cout << *p; // ERROR
      
      





 std::string_view s = "foo"s; cout << s[0]; // ERROR // :       std::string_view s = "foo"s // pset(s) = {"foo"s '} ; // pset(s) = {invalid}
      
      





 vector<int> v = get_ints(); for (auto i = v.begin(); i != v.end(); ++i) { // pset(i) = {v'} if (*i == 2) { v.erase(i); // pset(i) = {invalid} } // pset(i) = {v', invalid} } // ERROR: ++i for (auto i = v.begin(); i != v.end(); ) { if (*i == 2) i = v.erase(i); // OK else ++i; }
      
      





 std::optional<std::vector<int>> get_data(); //   ,  get_data() != nullopt for (int value : *get_data()) // ERROR cout << value; // *get_data() —     for (int value : std::vector<int>(*get_data())) // OK cout << value;
      
      





関数パラメーターの寿命の追跡



ポインターを返すC ++の関数の処理を開始すると、パラメーターの有効期間と戻り値の関係についてのみ推測できます。 関数が同じ型のPointerを受け入れて返す場合、関数は入力パラメーターの1つから戻り値を「取得」すると仮定されます。







 auto f(int* p, int* q) -> int*; // pset(ret) = {p', q'} auto g(std::string& s) -> char*; // pset(ret) = {s'}
      
      





どこからでも結果を取得する疑わしい機能は簡単に検出されます。







 std::reference_wrapper<int> get_data() { //    int i = 3; return {i}; // pset(ret) = {i'} } // pset(ret) = {invalid}
      
      





一時的な値はconst T&



パラメーターに渡すことができるため、結果がどこにもない場合を除き、それらは考慮されません。







 template <typename T> const T& min(const T& x, const T& y); // pset(ret) = {x', y'} //    const T&- //        auto x = 10, y = 2; auto& bad = min(x, y + 1); // pset(bad) = {x, temp} // pset(bad) = {x, invalid} cout << bad; // ERROR
      
      





 using K = std::string; using V = std::string; const V& find_or_default(const std::map<K, V>& m, const K& key, const V& def); // pset(ret) = {m', key', def'} std::map<K, V> map; K key = "foo"; const V& s = find_or_default(map, key, "none"); // pset(s) = {map', key', temp} ⇒ pset(s) = {map', key', invalid} cout << s; // ERROR
      
      





また、関数が(参照の代わりに)ポインターを受け入れる場合、nullptrになる可能性があり、nullptrと比較する前にこのポインターを使用できないと考えられています。







ライフタイムの結論



LifetimeはまだC ++標準の提案ではなく、C ++でライフタイムチェックを実装するための大胆な試みであり、Rustとは異なり、対応する注釈がなかったことを繰り返します。 最初は多くの誤検知がありますが、時間の経過とともにヒューリスティックが改善されます。







聴衆からの質問



Lifetimeグループチェックは、ダングリングポインターが存在しないことを数学的に正確に保証しますか?







理論的には、(新しいコードで)クラスと関数に大量の注釈を掛けることができ、その代わりにコンパイラはそのような保証を与えます。 ただし、これらのチェックは80:20の原則に従って開発されました。つまり、少数のルールを使用し、最小限の注釈を適用することで、ほとんどのエラーをキャッチできます。







メタクラス



何らかの方法でメタクラスは、それが適用されるクラスのコードを補完し、特定の条件を満たすクラスのグループの名前としても機能します。 たとえば、以下に示すように、メタクラスinterface



はすべての関数をパブリックにし、純粋に仮想化します。







昨年、ハーブサッターは彼の最初のメタクラスプロジェクトを作成しました( こちらを参照 )。 それ以来、現在提案されている構文は変更されました。







まず、メタクラスを使用するための構文が変更されました。







 //  interface Shape { int area() const; void scale_by(double factor); }; //  class(interface) Shape { … }
      
      





それは長くなりましたが、複数のメタクラスを一度に適用するための自然な構文があります: class(meta1, meta2)









メタクラスの説明



以前は、メタクラスはクラスを変更するための一連のルールでした。 現在、メタクラスはconstexpr関数であり、古いコード(コードで宣言されている)を受け取り、新しいクラスを作成します。







つまり、関数は1つのパラメーターを取ります。古いクラスに関するメタ情報(パラメーターのタイプは実装に依存します)、クラス要素(フラグメント)を作成し、 __generate



命令を使用して新しいクラスの本体に追加します。







フラグメントは、構成__fragment



__inject



idexpr(…)



を使用して生成できます。 この部分は標準化委員会に提出される前に変更されるため、スピーカーは目的に焦点を合わせないことを好みました。 名前自体は変更されることが保証されており、これを明確にするために二重下線が追加されています。 レポートの重点は、さらに進んだ例にありました。







インターフェース



 template <typename T> constexpr void interface(T source) { // source    //     .     //  ~X,  X —   . __generate __fragment struct X { virtual ~X noexcept {} }; //    static_assert, compiler.require   //   constexpr-. //      . compiler.require(source.variables().empty(), "interfaces may not contain data members"); // member_functions(), ,  tuple<…>,   for... for... (auto f : source.member_functions()) { // ,   —   / compiler.require(!f.is_copy() && !f.is_move(), "interfaces may not copy or move; consider a virtual clone()"); //   public   if (!f.has_default_access()) f.make_public(); // (1) // ,       protected/private compiler.require(f.is_public(), "interface functions must be public"); //     f.make_pure_virtual(); // (2) //   f     __generate f; } }
      
      





行(1)と(2)で元のクラスを変更すると思うかもしれませんが、変更しません。 元のクラスの関数をコピーして繰り返し、これらの関数を変更してから、新しいクラスに挿入することに注意してください。







メタクラスアプリケーション:







 class(interface) Shape { int area() const; void scale_by(double factor); }; //  : class Shape { public: virtual ~Shape noexcept {} public: virtual int area() const = 0; public: virtual void scale_by(double factor) = 0; };
      
      





ミューテックスのデバッグ



mutexで保護された非スレッドセーフデータがあるとします。 デバッグアセンブリで、各呼び出しで、現在のプロセスがこのミューテックスをロックしているかどうかを確認する場合、デバッグを容易にすることができます。 このために、単純なTestableMutexクラスが作成されました。







 class TestableMutex { public: void lock() { m.lock(); id = std::this_thread::get_id(); } void unlock() { id = std::thread::id{}; m.unlock(); } bool is_held() { return id == std::this_thread::get_id(); } private: std::mutex m; std::atomic<std::thread::id> id; };
      
      





さらに、MyDataクラスでは、すべてのパブリックフィールドが







 vector<int> v;
      
      





+ getterに置き換えます。







 private: vector<int> v_; public: vector<int>& v() { assert(m_.is_held()); return v_; }
      
      





関数についても、同様の変換を実行できます。







このようなタスクは、マクロとコード生成の助けを借りて解決されます。 Macro Herb Sutterは戦争を宣言しました:それらは安全ではなく、セマンティクス、名前空間などを無視します。 メタクラスでのソリューションの外観:







 constexpr void guarded_with_mutex() { __generate __fragment class { TestableMutex m_; // lock, unlock } } template <typename T, typename U> constexpr void guarded_member(T type, U name) { auto field = …; __generate field; auto getter = …; __generate getter; } template <typename T> constexpr void guarded(T source) { guarded_with_mutex(); for... (auto o : source.member_variables()) { guarded_member(o.type(), o.name()); } }
      
      





使い方:







 class(guarded) MyData { vector<int> v; Widget* w; }; MyData& x = findData("foo"); xv().clear(); // assertion failed: m_.is_held()
      
      





俳優



さて、いくつかのオブジェクトをミューテックスで保護しましょう。今ではすべてがスレッドセーフであり、正当性を主張するものはありません。 しかし、多くのスレッドが並行してオブジェクトに頻繁にアクセスできる場合、ミューテックスが過負荷になり、そのために大きなオーバーヘッドが発生します。







バギーミューテックスの問題に対する基本的な解決策はアクターの概念です。オブジェクトに要求キューがある場合、オブジェクトへのすべての呼び出しはキューに入れられ、特別なスレッドで次々に実行されます。







Activeクラスに、これらすべての実装(実際には、単一のスレッドを持つスレッドプール/エグゼキューター)が含まれるようにします。 さて、メタクラスは重複するコードを取り除き、すべての操作をキューに入れるのに役立ちます。







 class(active) ImageFilter { public: ImageFilter(std::function<void(Buffer*)> w) : work(std::move(w)) {} void apply(Buffer* b) { work(b); } private: std::function<void(Buffer*)> work; } //  : class ImageFilter { public: ImageFilter(std::function<void(Buffer*)> w) : work(std::move(w)) {} void apply(Buffer* b) { a.send([=] { work(b); }).join(); } private: std::function<void(Buffer*)> work; Active a; //   ,     work }
      
      





 class(active) log { std::fstream f; public: void info(…) { f << …; } };
      
      





財産



ほとんどすべての最新のプログラミング言語にはプロパティがあり、C ++に基づいてそれらを実装しなかった人は誰でも:Qt、C ++ / CLI、あらゆる種類のugいマクロ。 ただし、それらはC ++標準に追加されることはありません。それら自体が狭すぎる機能であると見なされているためです。 まあ、それらはメタクラスに実装できます!







 //  class X { public: class(property<int>) WidthClass { } width; }; //  class X { public: class WidthClass { int value; int get() const; void set(const int& v); void set(int&& v); public: WidthClass(); WidthClass(const int& v); WidthClass& operator=(const int& v); operator int() const; //   move! WidthClass(int&& v); WidthClass& operator=(int&& v); } width; };
      
      





独自のゲッターとセッターを設定できます。







 class Date { public: class(property<int>) MonthClass { int month; auto get() { return month; } void set(int m) { assert(m > 0 && m < 13); month = m; } } month; }; Date date; date.month = 15; // assertion failed
      
      





理想的には、 property int month { … }



を書きたいのですが、そのような実装でさえ、プロパティを発明するC ++拡張機能の動物園に取って代わります。







メタクラスの結論



メタクラスは、すでに複雑な言語の大きな新機能です。 それは価値がある? その利点の一部を次に示します。









聴衆からの質問



メタクラスをデバッグする方法は?







少なくともClangには、呼び出された場合、コンパイル時にクラスの実際の内容、つまりすべてのメタクラスを適用した後に取得される内容を出力する組み込み関数があります。







以前は、メタクラスでスワップやハッシュのような非メンバーを宣言できると言われていました。 彼女はどこに行きましたか?







構文はさらに開発されます。







概念がすでに標準化に採用されているのに、なぜメタクラスが必要なのですか?







これらは異なるものです。 クラスの一部を定義するにはメタクラスが必要であり、概念はクラスの例を使用してクラスが特定のパターンに一致するかどうかを確認します。 実際、メタクラスと概念はうまく機能します。 たとえば、イテレータの概念と、残りの部分で冗長な操作を定義する「典型的なイテレータ」のメタクラスを定義できます。








All Articles