毎日のC ++:CスタイルAPIの分離

私たちは皆、Cコードと簡単に統合できるC ++に感謝していますが、これらは2つの異なる言語です。







レガシーCは、現代のC ++にとって最も重い負担の1つです。 あなたはそのような負担を取り除くことはできませんが、それとともに生きることを学ぶことができます。 しかし、多くのプログラマーは生きるのではなく、苦しむことを好みます。 これについてお話します。







CとC ++のビジネスロジックを混在させないでください



少し前まで、私のお気に入りのコンポーネントに新しい挿入物があることに気づきました。 私のコードはテスター駆動開発の犠牲になりました。







ウィキペディアよると 、テスター駆動型開発は、要件がバグレポートまたは証言レビューによって決定される開発アンチメソドロジであり、プログラマーは症状のみを処理しますが、実際の問題は解決しません。

コードを短くして、C ++ 17に変換しました。 よく見て、ビジネスロジックフレームワークに余分なものがあるかどうかを考えてください。







bool DocumentLoader::MakeDocumentWorkdirCopy() { std::error_code errorCode; if (!std::filesystem::exists(m_filepath, errorCode) || errorCode) { throw DocumentLoadError(DocumentLoadError::NotFound(), m_filepath, errorCode.message()); } else { // Lock document HANDLE fileLock = CreateFileW(m_filepath.c_str(), GENERIC_READ, 0, // Exclusive access nullptr, // security attributes OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr //template file ); if (fileLock == INVALID_HANDLE_VALUE) { throw DocumentLoadError(DocumentLoadError::IsLocked(), m_filepath, "cannot lock file"); } CloseHandle(fileLock); } std::filesystem::copy_file(m_filepath, m_documentCopyPath); }
      
      





関数が何をするかを言葉で説明しましょう:









ここで機能の抽象化のレベルから何かが落ちると思いませんか?







不要なイラスト







抽象化のレイヤーを混在させないでください。ロジックの詳細レベルが異なるコードは、関数、クラス、またはライブラリの境界で分離する必要があります。 CとC ++を混在させないでください。これらは異なる言語です。







私の意見では、関数は次のようになります。







 bool DocumentLoader::MakeDocumentWorkdirCopy() { boost::system::error_code errorCode; if (!boost::filesystem::exists(m_filepath, errorCode) || errorCode) { throw DocumentLoadError(DocumentLoadError::NotFound(), m_filepath, errorCode.message()); } else if (!utils::ipc::MakeFileLock(m_filepath)) { throw DocumentLoadError(DocumentLoadError::IsLocked(), m_filepath, "cannot lock file"); } fs::copy_file(m_filepath, m_documentCopyPath); }
      
      





CとC ++が異なるのはなぜですか?



そもそも、彼らはさまざまな時期に生まれ、さまざまな重要なアイデアを持っています。









C ++では、エラーは例外によって処理されます。 Cではどのように扱われますか? 戻りコードを覚えている人は誰でも間違っています:標準C言語のfopen



は戻りコードでエラー情報を返しません。 さらに、Cの出力パラメーターはポインターによって渡され、C ++ではプログラマーがこれをthisられることがあります。 さらに、C ++には、リソース管理のRAIIイディオムがあります。







残りの違いはリストしません。 私たちは、C ++プログラマがC ++で記述し、次の目的でCスタイルのAPIの使用を強制されていることを事実として受け入れています。









しかし、使用とは「あらゆる場所で買い物をする」という意味ではありません。







ファイルを開く方法



ifstreamを使用している場合、エラー処理により、ファイルを開こうとすると次のようになります。







 int main() { try { std::ifstream in; in.exceptions(std::ios::failbit); in.open("C:/path-that-definitely-not-exist"); } catch (const std::exception& ex) { std::cout << ex.what() << std::endl; } try { std::ifstream in; in.exceptions(std::ios::failbit); in.open("C:/"); } catch (const std::exception& ex) { std::cout << ex.what() << std::endl; } }
      
      





最初のパスは存在せず、2番目のパスはディレクトリであるため、例外が発生します。 ただし、エラーテキストにはファイルへのパスも正確な理由もありません。 このようなエラーをログに書き込む場合、これはどのように役立ちますか?







Fstreamエラーのスクリーンショット







CスタイルAPIを使用する一般的なコードの動作は悪くなります。例外の安全性さえ保証されません。 以下の例では、挿入// ..



から例外をスローすると// ..



ファイルは閉じられません。







 //  ,    #if defined(_MSC_VER) #define _CRT_SECURE_NO_WARNINGS #endif int main() { try { FILE *in = ::fopen("C:/path-that-definitely-not-exist", "r"); if (!in) { throw std::runtime_error("open failed"); } // .. .. fclose(in); } catch (const std::exception& ex) { std::cout << ex.what() << std::endl; } }
      
      





ここで、このコードを使用して、CスタイルのAPIを使用している場合でも、C ++ 17の機能を示します。







そして、OOPが勧めているようにしないのはなぜですか?



さあ、試してみてください。 読み取り署名は次のように見えるため、ファイルから読み取ることができたバイト数を取得して見つけることができない別のiostreamを取得します。







 basic_istream& read(char_type* s, std::streamsize count);
      
      





iostreamを引き続き使用する場合は、tellgも呼び出してください。







 //      count   ,      filepath std::string GetFirstFileBytes(const std::filesystem::path& filepath, size_t count) { assert(count != 0); //  ,     std::ifstream stream; stream.exceptions(std::ifstream::failbit); //  : C++17   ifstream //    string,    wstring stream.open(filepath.native(), std::ios::binary); std::string result(count, '\0'); //    count    stream.read(&result[0], count); //  ,   ,  . result = result.substr(0, static_cast<size_t>(stream.tellg())); return result; }
      
      





C ++の同じタスクは2回の呼び出しで解決され、Cではfread



1回の呼び出しで解決されます! X用のC ++ラッパーを提供する多くのライブラリの中で、ほとんどの場合、このような制限が作成されるか、最適でないコードを記述せざるを得ません。 別のアプローチを示します。C++ 17の手続き型です。







ステップ1:RAII



ジュニアは、リソース管理のために独自のRAIIを作成する方法を常に知っているわけではありません。 しかし、私たちは知っています:







 namespace detail { // ,    struct FileDeleter { void operator()(FILE* ptr) { fclose(ptr); } }; } //  FileUniquePtr -   unique_ptr,  fclose using FileUniquePtr = std::unique_ptr<FILE, detail::FileDeleter>;
      
      





この機能により、 fopen2



関数で::fopen



関数をラップできます。







 //  ,    #if defined(_MSC_VER) #define _CRT_SECURE_NO_WARNINGS #endif //   ,   Unicode    UNIX-. FileUniquePtr fopen2(const char* filepath, const char* mode) { assert(filepath); assert(mode); FILE *file = ::fopen(filepath, mode); if (!file) { throw std::runtime_error("file opening failed"); } return FileUniquePtr(file); }
      
      





この関数にはまだ3つの欠点があります。









存在しないパスおよびディレクトリパスに対して関数を呼び出すと、次の例外テキストが表示されます。







エラーのスクリーンショット







ステップ2:エラー情報を収集する



まず、OSからエラーの原因を見つける必要があります。次に、コールスタックに沿った飛行中にエラーのコンテキストを失わないために、発生したパスを示す必要があります。







そして、ここで私は認めなければなりません:ジュニアだけでなく、多くのミドルとシニアの女性も、errnoを扱う方法とそれがスレッドセーフである方法を知りません。 これを書きます:







 //  ,    #if defined(_MSC_VER) #define _CRT_SECURE_NO_WARNINGS #endif //   ,   Unicode    UNIX-. FileUniquePtr fopen3(const char* filepath, const char mode) { using namespace std::literals; //   ""s. assert(filepath); assert(mode); FILE *file = ::fopen(filepath, mode); if (!file) { const char* reason = strerror(errno); throw std::runtime_error("opening '"s + filepath + "' failed: "s + reason); } return FileUniquePtr(file); }
      
      





存在しないパスおよびディレクトリパスに対して関数を呼び出すと、より正確な例外テキストが取得されます。







詳細なエラーのスクリーンショット







ステップ3:ファイルシステムの実験



C ++ 17には多くの小さな改善が加えられており、そのうちの1つはstd::filesystem



モジュールです。 boost::filesystem



よりも優れていboost::filesystem











私たちの場合、ファイルシステムはエンコーディングに敏感ではないユニバーサルパスクラスをもたらしました。 これにより、WindowsでUnicodeパスを透過的に処理できます。







 //  VS2017  filesystem    experimental #include <cerrno> #include <cstring> #include <experimental/filesystem> #include <fstream> #include <memory> #include <string> namespace fs = std::experimental::filesystem; FileUniquePtr fopen4(const fs::path& filepath, const char* mode) { using namespace std::literals; assert(mode); #if defined(_WIN32) fs::path convertedMode = mode; FILE *file = ::_wfopen(filepath.c_str(), convertedMode.c_str()); #else FILE *file = ::fopen(filepath.c_str(), mode); #endif if (!file) { const char* reason = strerror(errno); throw std::runtime_error("opening '"s + filepath.u8string() + "' failed: "s + reason); } return FileUniquePtr(file); }
      
      





このようなコードは書くのが難しく、共有ライブラリの経験豊富なエンジニアの1人が一度書く必要があることは、私には明らかです。 ジュニアはそのようなジャングルに登ってはいけません。







未来に目を向ける:プリプロセッサのない世界



次に、2017年6月に、ほとんどの場合コンパイラーがコンパイルされないコードを示します。 いずれにせよ、VS2017では、constexpr ifはまだ実装されておらず、何らかの理由でGCC 8がifブランチをコンパイルし、次のエラーを生成します:







コンパイルエラーのスクリーンショット







はい、はい、ソースを条件付きでコンパイルする新しい方法を提供するC ++ 17からの場合はconstexprについて説明します。







 FileUniquePtr fopen5(const fs::path& filepath, const char* mode) { using namespace std::literals; assert(mode); FILE *file = nullptr; //   path::value_type -   wchar_t,  wide- //  Windows      UTF-16,   . // : wchar_t   UTF-16   Windows. if constexpr (std::is_same_v<fs::path::value_type, wchar_t>) { fs::path convertedMode = mode; file = _wfopen(filepath.c_str(), convertedMode.c_str()); } //    ,    UTF-8    Unicode else { file = fopen(filepath.c_str(), mode); } if (!file) { const char* reason = strerror(errno); throw std::runtime_error("opening '"s + filepath.u8string() + "' failed: "s + reason); } return FileUniquePtr(file); }
      
      





これは素晴らしい機会です! モジュールや他のいくつかの機能がC ++言語に追加された場合、C言語のプリプロセッサを悪夢として忘れて、それなしで新しいコードを書くことができます。 さらに、モジュールを使用すると、コンパイル(レイアウトなし)がはるかに高速になり、主要なIDEがより短い遅延で自動補完に応答します。







手続き型の長所



OOPは業界とアカデミックコードの機能的アプローチを支配しますが、手続き型のファンはまだ喜ぶべきことがあります。









標準のCライブラリ、WinAPI、CURL、またはOpenGLのすべての関数を同様の手続きスタイルでラップすることをお勧めします。







まとめると



C ++ Russia 2016およびC ++ Russia 2017で、すばらしいスピーカーのミハイル・マトロソフが、希望するすべての人、サイクルを使用する必要がない理由、およびサイクルなしで生活する方法を示しました。









私の知る限り、マイケルのインスピレーションは、Sean Parentによる2013 C ++調味料レポートでした。 レポートでは、次の3つのルールが強調されました。









毎日のC ++コードの別の4番目のルールを追加します。 CC-Plus-Plusで記述しないでください。 ビジネスロジックとCを混在させないでください。









理由はこの記事で美しく示されています。 それらを次のように定式化します。







C / C ++で完全に信頼できるコードを書くことができるのは、真のヒーローだけです。 職場で毎日ヒーローが必要な場合、問題があります。



All Articles