宣言型C ++プログラミング

金曜日には、緊急の問題がなく、緊急ではない怠で、魂のために何かが欲しいときなど、無料の夜がありました。 魂のために、1か月以上前に行われたCppCon 2015のレポートを見ることにしました。 原則として、ライブビデオレポートに十分な時間はありませんが、それはすべてこのように起こりました-1か月が過ぎ、C ++-17がすでに鼻にあり、会議は興味深いはずでしたが、誰もそれについて何も書いていなかったので、それは夜でした一般に、私はすぐに注目を集めた最初の見出しにマウスを突っ込んだ: Andrei Alexandrescu「宣言的制御フロー」と素敵な夜を過ごした後、私はhabrasocietyと無料で語り直すことにしました。

C ++ Explicit Flow Controlの通常の動作を思い出してください。ファイルをコピーするためのトランザクション的に安定した関数を作成します。つまり、成功するか、何らかの理由で失敗するが、副作用がないという意味で安定しています。 (美しい表現-成功した失敗)。 特にboost :: filesystemを使用する場合、タスクは簡単に見えます。
void copy_file_tr(const path& from, const path& to) { path tmp=to+".deleteme"; try { copy_file(from, tmp); rename(tmp, to); } catch(...) { ::remove(tmp.c_str()); throw; } }
      
      



コピー中に何が起こっても、一時ファイルは削除されます。これが必要なことです。 ただし、意味のあるコードの3行だけを詳しく見る場合、残りはtry / catchによる関数呼び出しの成功、つまり実行の手動制御を確認することです。 ここでのプログラム構造は、問題の実際の論理を反映していません。 別の不快な瞬間は、このコードがここで説明されていない呼び出される関数のプロパティに大きく依存しているため、rename()関数はアトミック(トランザクション的に安定)であると想定され、remove()は例外をスローすべきではありません(why :: remove()がboostの代わりに使用されます: :filesystem :: remove())。さらに悪化させて、ペア関数move_file_trを書きましょう。
 void move_file_tr(const path& from, const path& to) { copy_file_tr(from, to); try { remove(from); } catch(...) { ::remove(to.c_str()); throw; } }
      
      



ここではすべて同じ問題が発生します。このような小さなコードの一部に別のtry / catchブロックを追加する必要がありました。 さらに、ここでも、このようなコードのスケーリングがいかに不十分であるか、各ブロックが独自のスコープに入るか、ブロックの交差が不可能であるかなどにすでに気づいています。 これらすべてがまだあなたを納得させていない場合、 標準はtry / catchの手動使用を最小限に抑えることを推奨しています。なぜなら、「冗長かつ非自明な使用はエラーが発生しやすい」からです。 。

代わりに、宣言型スタイルは目標の説明に焦点を合わせ、それを達成する方法に関する詳細な指示は最小限に抑えられますが、各ステップの実行を直接制御することなく、正しい方法でコードが実行されます。 それは幻想のように聞こえるかもしれませんが、そのような言語は私たちの周りにあり、私たちはためらうことなく毎日それらを使用しています。 見てください-SQL、make、regex、それらはすべて本質的に宣言的です。 この効果を達成するためにC ++で何を使用できますか?

RAIIとデストラクタは暗黙的に呼び出されるため、本質的に宣言的です。 ScopeGuardを使用してSCOPE_EXITマクロがどのように配置されるかを見てみましょう。これは実際にはかなり古いトリックです。バージョン1.38以降、同じ名前のマクロがboostに存在していると言えば十分です。 それでもなお、母親の教えの繰り返し:
 namespace detail { enum class ScopeGuardOnExit {}; template<typename<Fun> ScopeGuard<Fun> operator+ (ScopeGuardOnExit, Fun&& fn) { return ScopeGuard<Fun>(std::forward<Fun>(fn)); } } #define SCOPE_EXIT \ auto ANONIMOUS_VARIABLE(SCOPE_EXIT_STATE) \ = ::detail::ScopeGuardOnExit + (&)[] }
      
      



実際、これはラムダ関数の定義の半分であり、呼び出されたときに本体を追加する必要があります。

ここではすべてが非常に簡単です。ScopeGuardを含む匿名変数が作成されます。ScopeGuardには、マクロ呼び出しの直後に定義されたラムダ関数が含まれ、この関数のデストラクタで呼び出される関数は、遅かれ早かれ、スコープを離れるときに呼び出されます。 (肺で空気がなくなった、そうでなければ私はいくつかの副次条項を追加するだろう)

完全を期すために、これは補助マクロがどのように見えるかです:
 #define CONACTENATE_IMPL(s1,s2) s1##s2 #define CONCATENATE(s1,s2) CONCATENATE_IMPL(s1,s2) #define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__COUNTER__)
      
      



この設計を使用して、おなじみのC ++コードは前例のない機能を一度に取得します。
 void fun() { char name[] = "/tmp/deleteme.XXXXXX"; auto fd = mkstemp(name); SCOPE_EXIT { fclose(fd); unlink(name); }; auto buf = malloc(1024*1024); SCOPE_EXIT { free(buf); }; ... }
      
      



したがって、宣言スタイルへの完全な移行のために、このようなマクロをさらに2つ定義するだけで十分であると主張されています。SCOPE_FAILとSCOPE_SUCCESS。このトリプルを使用して、論理的に意味のあるコードと詳細な制御命令を分離できます。 これを行うには、デストラクタが通常に呼び出されたか、スタックをアンワインドした結果として呼び出されたかを知ることが必要かつ十分です。 そして、そのような関数はC ++にあります-bool uncaught_exception() 、catchブロック内から呼び出された場合はtrueを返します 。 ただし、不快なニュアンスが1つあります。現在のバージョンのC ++のこの関数は壊れており、常に正しい値を返すとは限りません。 実際には、デストラクタの呼び出しがスタックの巻き戻しの一部であるか、catchブロック内で作成されたスタック上の通常のオブジェクトであるかは区別されません。これについては、 元のソースから参照できます。 C ++-17では、この関数は廃止されたと公式に宣言され 、代わりに別の関数が導入されます-int uncaught_exceptions() (2つの違いを見つけます)、呼び出されたネストされたハンドラの数を返します SCOPE_SUCCESSまたはSCOPE_FAILを呼び出すかどうかを正確に示すヘルパークラスを作成できるようになりました。
 class UncaughtExceptionCounter { int getUncaughtExceptionCount() noexcept; int exceptionCount_; public: UncaughtExceptionCounter() : exceptionCount_(std::uncaught_exceptions()) {} bool newUncaughtException() noexcept { return std::uncaught_exceptions() > exceptionCount_; } };
      
      



このクラス自体がRAIIを使用してコンストラクターで状態をキャプチャすることも面白いです。

これで、成功または失敗した場合に呼び出される本格的なテンプレートを描画できます。
 template <typename FunctionType, bool executeOnException> class ScopeGuardForNewException { FunctionType function_; UncaughtExceptionCounter ec_; public: explicit ScopeGuardForNewException(const FunctionType& fn) : function_(fn) {} explicit ScopeGuardForNewException(FunctionType&& fn) : function_(std::move(fn)) {} ~ScopeGuardForNewException() noexcept(executeOnException) { if (executeOnException == ec_.isNewUncaughtException()) { function_(); } } };
      
      



実際、興味深いものはすべてデストラクタに集中しています。例外カウンタの状態がテンプレートパラメータと比較され、内部ファンクタを呼び出すかどうかが決定されます。 また、SCOPE_FAILは例外に対して安全でなければならず、SCOPE_SUCCESSは完全に例外を完全にスローできるため、同じテンプレートパラメーターがデストラクタのシグネチャをエレガントに定義する方法にも注意してください: noexcept(executeOnException) 。 私の意見では、C ++をまさに私の大好きな言語にしているのは、そのような小さなアーキテクチャの詳細です。

次に、SCOPE_EXITのように新しいマクロを定義するなど、すべてが簡単になります。
 enum class ScopeGuardOnFail {}; template <typename FunctionType> ScopeGuardForNewException< typename std::decay<FunctionType>::type, true> operator+(detail::ScopeGuardOnFail, FunctionType&& fn) { return ScopeGuardForNewException< typename std::decay<FunctionType>::type, true >(std::forward<FunctionType>(fn)); } #define SCOPE_FAIL \ auto ANONYMOUS_VARIABLE(SCOPE_FAIL_STATE) \ = ::detail::ScopeGuardOnFail() + [&]() noexcept
      
      



同様に、SCOPE_EXITについても

元の例がどのように見えるか見てみましょう。
 void copy_file_tr(const path& from, const path& to) { bf::path t = to.native() + ".deleteme"; SCOPE_FAIL { ::remove(t.c_str()); }; bf::copy_file(from, t); bf::rename(t, to); } void move_file_tr(const path& from, const path& to) { bf::copy_file_transact(from, to); SCOPE_FAIL { ::remove(to.c_str()); }; bf::remove(from); }
      
      



コードはより透明に見え、さらに各行は何かを意味します。 SCOPE_SUCCESSの使用例と、このマクロが例外をスローする理由のデモを次に示します。
 int string2int(const string& s) { int r; SCOPE_SUCCESS { assert(int2string(r) == s); }; ... return r; }
      
      



したがって、非常に小さな構文上の障壁により、C ++のイディオムに別の宣言スタイルを追加することから分離されます。

一人称の結論

これはすべて、近い将来に私たちを待っているかもしれないものについての特定の思考につながります。 まず第一に、レポート内のすべてのリンクが新しいものとはほど遠いということに私は感銘を受けました。 たとえば、SCOPE_EXITはboost.1.38に存在します。つまり、ほぼ10年間存在し、ScopeGuardに関するAlexandrescuの記事は、2000年にドブス博士にすでに公開されています。 アレクサンドレスクは先見者と預言者としての評判を持っていることを思い出したい。なぜなら、彼がコンセプトのデモンストレーションとして彼によって作成したロキ図書館はブースト:: mplの基礎を形成し、それからほぼ完全に新しい標準に入り、そのずっと前に、実際にメタプログラミングのイディオムを設定したからだ。 一方、アレクサンドレスク自身は最近D言語の開発に主に従事しており、前述の3つの構造( スコープ終了、スコープの成功、スコープの失敗)はすべて言語構文の一部であり、長い間強力な位置を占めてきました。

もう1つの興味深い点は、標準ライブラリのRangesと呼ばれる同じ会議でのEric Nieblerの講演です。 範囲はD言語のもう1つの標準的な概念であり、イテレータの概念をさらに発展させたものであることを思い出してください。 さらに、レポート自体は、実際には、すばらしい記事であるHSTeohの範囲付きコンポーネントプログラミングの翻訳(DからC ++へ)です。

したがって、C ++には他の言語の概念が積極的に含まれるようになりましたが、彼自身がそれを始めました。 いずれにせよ、今後のC ++-17は定期的な更新ではないようです。 歴史の教訓を考えると、17年目は退屈ではなく、ポップコーン、パイナップル、ヘーゼルライチョウでいっぱいです。

文学

ここでは、すでに投稿に含まれているリンクが1つの場所に集められています。
  1. 元の音声レポート
  2. CppCon 2015資料へのリンク
  3. Alexandrescuレポートのスライド
  4. 元のScopeGuard 2000の記事へのリンク
  5. Boostドキュメント:: ScopeExit
  6. uncaught_exception()を変更するためのハーブサッターの提案
  7. 興味のあるDの範囲に関する元の記事 、この言語の1つの側面への良い非公式の紹介



All Articles