私たちは皆、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); }
関数が何をするかを言葉で説明しましょう:
- ファイルが存在しない場合、NotFoundコードとファイルへのパスで例外がスローされます
- それ以外の場合、指定された読み取りパスでファイルを開き、セキュリティ属性なしで、排他アクセス権を使用して、可能であれば既存のファイルを開き、新しいファイルを作成するときに通常のファイル属性を設定し、テンプレートファイルを使用しない
- 前の操作が失敗した場合、ファイルを閉じてIsLockedコードで例外をスローします
- それ以外の場合は、ファイルを閉じてコピーします
ここで機能の抽象化のレベルから何かが落ちると思いませんか?
抽象化のレイヤーを混在させないでください。ロジックの詳細レベルが異なるコードは、関数、クラス、またはライブラリの境界で分離する必要があります。 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 ++では、エラーは例外によって処理されます。 Cではどのように扱われますか? 戻りコードを覚えている人は誰でも間違っています:標準C言語のfopen
は戻りコードでエラー情報を返しません。 さらに、Cの出力パラメーターはポインターによって渡され、C ++ではプログラマーがこれをthisられることがあります。 さらに、C ++には、リソース管理のRAIIイディオムがあります。
残りの違いはリストしません。 私たちは、C ++プログラマがC ++で記述し、次の目的でCスタイルのAPIの使用を強制されていることを事実として受け入れています。
- OpenGL、Vulkan、cairoおよびその他のグラフィックスAPI
- CURLおよびその他のネットワークライブラリ
- winapi、freetype、およびその他のシステムレベルライブラリ
しかし、使用とは「あらゆる場所で買い物をする」という意味ではありません。
ファイルを開く方法
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番目のパスはディレクトリであるため、例外が発生します。 ただし、エラーテキストにはファイルへのパスも正確な理由もありません。 このようなエラーをログに書き込む場合、これはどのように役立ちますか?
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つの欠点があります。
- ポインターによってパラメーターを受け取ります
- 例外には詳細が含まれていません
- Windows上のUnicodeパスは処理されません
存在しないパスおよびディレクトリパスに対して関数を呼び出すと、次の例外テキストが表示されます。
ステップ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
:
- 2038年の問題を解決し、ブーストで::ファイルシステムは解決されません
- UTF-8パスを取得する明確な方法がありますが、多くのライブラリ(SDL2など)にはUTF-8パスが必要です。
-
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関数をラップできます
fopen4
関数は引き続きCスタイルのフラグ、モード、その他のトリックを使用しますが、リソースを確実に管理し、すべてのエラー情報を収集し、パラメーターを慎重に受け入れます。 - fopen関数のドキュメントはまだラッパーに関連しており、他のプログラマーの検索、理解、再利用を大幅に促進します。
標準のCライブラリ、WinAPI、CURL、またはOpenGLのすべての関数を同様の手続きスタイルでラップすることをお勧めします。
まとめると
C ++ Russia 2016およびC ++ Russia 2017で、すばらしいスピーカーのミハイル・マトロソフが、希望するすべての人、サイクルを使用する必要がない理由、およびサイクルなしで生活する方法を示しました。
私の知る限り、マイケルのインスピレーションは、Sean Parentによる2013 C ++調味料レポートでした。 レポートでは、次の3つのルールが強調されました。
- 低レベルのforおよびwhileループを記述しない
- STL / Boostのアルゴリズムと他のツールを使用する
- 最終製品が収まらない場合は、別の機能でサイクルをラップします
- new / deleteを直接操作しないでください
- これについての詳細は、 新規および削除なしのミハイルマトロソフC ++のレポートで
- ミューテックスやスレッドなどの低レベルの同期プリミティブを使用しないでください
毎日のC ++コードの別の4番目のルールを追加します。 CC-Plus-Plusで記述しないでください。 ビジネスロジックとCを混在させないでください。
- Cを少なくとも1層の断熱材で包みます。
- 非同期コードについて話している場合は、2つの層にラップします。最初の層はCを分離し、2番目の層は同期プリミティブとスレッドのタスクシェディングを隠します
理由はこの記事で美しく示されています。 それらを次のように定式化します。
C / C ++で完全に信頼できるコードを書くことができるのは、真のヒーローだけです。 職場で毎日ヒーローが必要な場合、問題があります。