We all appreciate C ++ for easy integration with C code. And yet, these are two different languages.
Heritage C is one of the heaviest burdens for modern C ++. One cannot get rid of such a burden, but one can learn to live with it. However, many programmers prefer not to live, but to suffer. We will talk about this.
Not so long ago, I accidentally noticed a new insert in my favorite component. My code has become a victim of tester-driven development.
According to Wikipedia , Tester-driven development is an anti-methodology of development, in which requirements are determined by bug reports or testers' testimonials, and programmers only treat symptoms, but do not solve real problems.
I shortened the code and translated it into C ++ 17. Take a good look and think, is there anything left in the business logic:
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); }
Let's describe in words what the function does:
Do not you think that something here falls out of the level of abstraction function?
Do not mix layers of abstraction, code with different levels of logic detailing should be separated by the boundaries of a function, class, or library. Do not mix C and C ++, these are different languages.
In my opinion, the function should look like this:
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); }
To begin with, they were born at different times and they have different key ideas:
In C ++, errors are handled by exceptions. How are they handled in C? Who remembered the return codes is wrong: the standard for the C language fopen
function does not return information about the error in the return codes. Further, out-parameters in C are passed by pointer, and in C ++ the programmer can be scolded for this. Further, in C ++ there is an RAII idiom for resource management.
We will not list the other differences. Just accept as the fact that we, C ++ programmers, write in C ++ and are forced to use the C-style API for:
But using does not mean “shoving in all places”!
If you are using ifstream, then an error handling attempt to open the file looks like this:
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; } }
Since the first path does not exist, and the second is a directory, we will get exceptions. But there is no file path or exact reason in the text of the error. If you write such an error in the log, how will this help you?
Typical code using C-style API behaves worse: it does not even guarantee the security of exceptions. In the example below, when throwing an exception from the insert // ..
file will never be closed.
// , #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; } }
And now we take this code and show what C ++ 17 is capable of, even if we have a C-style API.
Go ahead, try. You will have another iostream, in which you cannot just take and find out how many bytes you managed to read from the file, because the read signature looks like this:
basic_istream& read(char_type* s, std::streamsize count);
And if you still want to use iostream, please also call 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; }
The same problem in C ++ is solved by two calls, and in C - by one call to fread
! Among the many libraries offering C ++ wrapper for X, most create such restrictions or force you to write non-optimal code. I'll show a different approach: procedural style in C ++ 17.
Juniors do not always know how to create their own RAII for resource management. But we know:
namespace detail { // , struct FileDeleter { void operator()(FILE* ptr) { fclose(ptr); } }; } // FileUniquePtr - unique_ptr, fclose using FileUniquePtr = std::unique_ptr<FILE, detail::FileDeleter>;
This feature allows you to wrap the function ::fopen
in the function fopen2
:
// , #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); }
This function still has three drawbacks:
If we call a function for a non-existent path and for a catalog path, we get the following exception texts:
Firstly, we need to find out from the OS the reason for the error, secondly, we must indicate the path it originated in order not to lose the context of the error in the process of flying through the call stack.
And here we must admit: not only the juniors, but also many midli and Signori do not know how to work with errno correctly and how much it is thread-safe. We will write this:
// , #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); }
If we call a function for a non-existent path and for a catalog path, we will get more accurate exception texts:
C ++ 17 brought many small improvements, and one of them is the std::filesystem
module. It is better than boost::filesystem
:
boost::filesystem
implementation contains dangerous games with pointer dereferencing, there are many Undefined BehaviorFor our case, filesystem brought the universal, non-encoding-sensitive class path. This allows you to transparently handle Unicode paths on Windows:
// 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); }
It seems to me obvious that such code is difficult to write and that one of experienced engineers in the general library should write it once. Junior in such a jungle is not worth climbing.
Now I will show you the code, which in June 2017, most likely, will not compile any compiler. In any case, in VS2017, constexpr if is not yet implemented, but for some reason GCC 8 compiles the if branch and gives the following error:
Yes, yes, it's about constexpr if from C ++ 17, which offers a new way to conditionally compile the source code.
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); }
This is an amazing opportunity! If modules and a few more features are added to C ++, we can forget the C preprocessor as a bad dream and write new code without it. In addition, with modules, compilation (without linking) will be much faster, and leading IDEs will respond to auto-completion with less delay.
Although the PLO rules in the industry, and in the academic code it is a functional approach, the procedural style fans still have something to rejoice about.
fopen4
in semantics: our fopen4
function still uses flags, mode and other C-style tricks, but reliably manages resources, collects all error information and carefully takes parametersI recommend all the functions of the standard C library, WinAPI, CURL or OpenGL to wrap in a similar procedural style.
In C ++ Russia 2016 and C ++ Russia 2017, a wonderful speaker, Mikhail Matrosov, showed everyone why there’s no need to use cycles and how to live without them:
As far as we know, the inspiration for Michael was the 2013 report "C ++ Seasoning" by Sean Parent. The report highlighted three rules:
I would add one more, fourth rule of everyday C ++ code. Do not write in CBC Plus. Do not mix business logic and C language.
The reasons are well illustrated in this article. We formulate them like this:
Only a true hero can write an absolutely reliable C / C ++ code. If you need a hero every day at work, you have a problem.
Source: https://habr.com/ru/post/331100/
All Articles