הייתי בשיחת מסדרון עם קולגה, והעלתי את הייתרונות של ה GSL של מייקרוסופט ביחד עם ה expected ומרכיבי השפה כתחליף ל SAL בשימוש תאגידי.
גילוי נאות, אני פחות אוהב את SAL בתחומי ה C++ כאשר מדובר על קוד יחסית חדש, בשפת C הוא דווקא מועיל מצויין לטעמי. ב C++ ? אני חושב שהוא פחות מתאים. מדוע ? ב C++ יש לנו יכולות של השפה הכל מ Concepts ו Constrainrts , ביחד עם Contracts דרך טיפוסים וסיפריות, הם מספקים פתרונות שעדיף היה אילו היו משומשים על ידי הבודק הסטטיסטי מאשר ב SAL, עכשיו נכון שזה לא עובד בכל הקומפיילרים , ונכון שזה דברים שדורשים לימוד, אבל אני חושב ששימוש בכלי השפה כמה שיותר עדיפים על שימוש ב SAL ושאר חלקים חיצוניים לשפה, עכשיו שיהיה ברור יש מקום ל SAL ושימוש בבודקים סטיסטיים כאלה, זה משהוא מאוד חשוב ואני משתמש בהם ברמה יומיומית, אני אפילו אומר שצריך להפעיל את הבודקים הסטטיסטים של VS שיבדקו את הקוד בזמן הבנייה, הבודק הסטטיסטי של VS צריך בקשה מפורשת לבצע בדיקות עבור C++ Core Guideline, ועל מנת להבהיר , ה GSL אינו בודק סטטיסטי זו ספרייה שמקטינה את כמות הבעיות שעושים בקוד.
האמת אני מתלהב מהרעיון של ה GSL, שהוא למעשה מימוש של מייקרוסופט עבור ה C++ Software Guidelines וההטמעה שיש ב VS בשבילו, עכשיו אני יודע שGSL לא מתאים לכל מי שכותב ב C++ ואחת הסיבות היא עקומת הלימוד לחלק מהכותבים ביחד עם העובדה שזה משתמש ב Google Test, ושלא לדבר על זה שזה לא עוזר אם אנחנו לא יכולים לשנות חתימות או מגבלות runtime, אבל במקרה שלי אין לי את הבעייה הזאת.
הייתי שמח מאוד שיהיו לי עוד יכולות לתאר באמצעות c++ atributes עוד מרכיבים של ההגדרה ביחד עם בדיקה טובה יותר והטמעה עוד יותר טוב בכלל הקומפיילרים. ישנם גם דברים שחסרים לי ב GSL לעומת SAL, לדוגמה - pre/post conditions, וגם העובדה שאני לא מצאתי דרך שניתן לאפיין סוגי מצביעים בקלות כמו שיש ב SAL ושהבודק הסטטיסטי של VS ישתמש בו כהלכה.
הייתי שמח מאוד שיהיו לי עוד יכולות לתאר באמצעות c++ atributes עוד מרכיבים של ההגדרה ביחד עם בדיקה טובה יותר והטמעה עוד יותר טוב בכלל הקומפיילרים. ישנם גם דברים שחסרים לי ב GSL לעומת SAL, לדוגמה - pre/post conditions, וגם העובדה שאני לא מצאתי דרך שניתן לאפיין סוגי מצביעים בקלות כמו שיש ב SAL ושהבודק הסטטיסטי של VS ישתמש בו כהלכה.
מאוד בגדול ב SAL אנחנו מוסיפים אנוטציות, מעין הערות, זה מחייב ללמוד משהוא דמויי שפה שלמה בשביל לכתוב ב C++, יש דברים הכל מבדקית טיפוסי חזרה, דרך תיאור שמאפשר לבודק הסטטי למצוא בעיות לפי התיאור שכתבנו מסביב לערכי החזרה של הפונקציה והמאפיינים של הפרמטרים של הפונקציה. למעשה בכל פרוייקט גדול המיועד לסביבה וינדוס שאני מכיר יש תמיד שימוש בSAL או מוצר מתחרה שלהם, הגישה הזאת דורשת עבודה, אבל היא שווה את זה.
הנה דוגמא לפונקציה עם SAL:
//Peform conditional library function ,based on system avilibility, error flows are expressed via return type. //success flow are either library_result_t::OK or library_result_t::valid_for_only_10_minutes; //invalid cases would may be caused of a non initlized system, invalid frequency or incorrect voltage _Success_(return == library_result_t::OK || return == library_result_t::valid_for_only_10_minute) library_result_t library_conditional_functionality(type_t & r_resultValue);
הפונקציה שלפנינו, מכילה אנוטציה המציינת מתי הפונקציה שלנו החזירה ערך הצלחה לביצוע העבודה, ויש לה פרמטר בו היא מכניסה את הערך או מהות העבודה. כאשר אנחנו משתמשים בגישה הזאת יש לנו שני ערכים מנותקים שאיתם אנו צריכים לעבוד, לשיטה הזאת יש ייתרונות וחסרונות, לפעמים יש עדיפות ששני המושגים האלה יהיו ביחד.
בפוסט הזה אני מציג איך std::expected שמאפשר לנו לאחד את ערך החזרה (נכונות ביצוע העבודה) מהפונקציה, ביחד עם פרמטר המגדיר את הערך שייצרנו, שזה רכיב מאוד קטן אבל נדרש בגישות עבודה. למשל בעבר או שהיינו משתמשים ב enum ביחד עם ערך או בצורה מופשטת כאשר טיפוס החזרה מהפונקציה היא נכונות הפונקציה, ואחד הפרמטרים היו ערך ההחזרה או שימוש במנגונים כמו std::optional או אפילו מעטפות של ערך החזרה עם נכונות החזרה כערך החוזר מהפונקציה.
לשם הדוגמה, נניח ויש לנו מבנה שאורז קופסאות, ערך החזרה הוא המצב שמגדיר שאנחנו הצלחצנו לארוז למשל, או נכשלנו לארוז, בעוד הפרמטר שמגדיר את הערך , זו הקופסא שיוצאת מהמבנה.
חשוב להבין שמגנון ה std::expected אינו בא להחליף exception והוא לא חלק מה static analisys כמו ה SAL, זה חלק מהשפה, זו אבן בניין, בשביל הבדיקה הסטטית יש לנו את ב VS את הכלי שעובד מול ההגדרות שכתבנו עם GSL, מנגנון ה std::expected מאפשר לנו לבדוק טיפוסי חזרה ובדיקה מאוד נוחה (יחסית לגרסאות ישנות יותר של C++ ) להתאמה לצרכים שלנו.
איך היינו עושים זאת לפני שהיה לנו את זה?
למשל עם optional :
למשל עם optional :
דוגמאות למה שאני מדבר: שימוש בoptional ביחד עם לוגיקה של ערך חוזר מהפונקציה ופרמטר שמתעדכן.
library_result_t library_conditional_functionality(type_t & r_resultValue); std::optional<std::string> single_success_flow() { type_t library_result_value{}; const library_result_t result = library_conditional_functionality(library_result); if (conditional_function(result)) { return convert_to_string(library_result_value); } return {}; }או לחילופן גישה המשתמשת בערכי חזרה אבל במצב הצלחה מעכנת פרמטר לפונקציה
//Peform conditional library function ,based on system avilibility, error flows are expressed via return type. //success flow are either library_result_t::OK or library_result_t::valid_for_only_10_minutes; //invalid cases would may be caused of a non initlized system, invalid frequency or incorrect voltage library_result_t library_conditional_functionality(type_t & r_resultValue);כאשר יש שימוש ב SAL אנחנו נזהה את זה בצורה הבאה:
//Peform conditional library function ,based on system avilibility, error flows are expressed via return type. //success flow are either library_result_t::OK or library_result_t::valid_for_only_10_minutes; //invalid cases would may be caused of a non initlized system, invalid frequency or incorrect voltage _Success_(return == library_result_t::OK || return == library_result_t::valid_for_only_10_minute) library_result_t library_conditional_functionality(type_t & r_resultValue);כאשר מגיע אליינו ה std::expected הוא מאפשר לנו תחליף מאוד נוח ל std::optional
דוגמית קוד ללא שימוש ב GSL או SAL כלל, זה מגיע רק להראות את השימוש ב std::expected:
//Code adjusted by Boris Shtrasman to express std::expected idiom to catch errors, this is for training purposes only //This code intentionally does not use proper exception handeling, SAL or GSL //This code exist to show the proper use of return type logic with use of std::expected in contrast to exception flows and SAL. #include <cmath> #include <expected> #include <iomanip> #include <iostream> #include <string_view> enum class extracting_double_from_prefix_error_types { value_too_large_to_store_in_type, invalid_input, overflow }; enum class result_validity_t { OK = 0, ERROR =1, LIBRARY_ERROR = 2, INVALID_COMPILER_SETTINGS = 3, }; std::expected<double, extracting_double_from_prefix_error_types> extract_longest_double_from_prefix(std::string_view& str) { if (str.empty()) { return std::unexpected(extracting_double_from_prefix_error_types::invalid_input); } const char* begin = str.data(); char* end_ptr_would_hold_error; double retval = std::strtod(begin, &end_ptr_would_hold_error);//This may or may not set errno in case of error! if (begin == end_ptr_would_hold_error) { return std::unexpected(extracting_double_from_prefix_error_types::invalid_input); } if (std::isinf(retval)) { return std::unexpected(extracting_double_from_prefix_error_types::overflow); } //always remember to have your constannts on your left side, that way you have less chances to do a stupid bug of asigment instead of comparison if (HUGE_VAL == retval) { return std::unexpected(extracting_double_from_prefix_error_types::value_too_large_to_store_in_type); } return retval; } //return OK in case of success, and print the value to the screen //return a non OK value in case of any failure //intentiional no exception catching, nor SEH catching result_validity_t treat_single_conversion(std::ostream& r_debugstream,std::string_view str) { if (str.empty()) { return result_validity_t::ERROR; } //this extract a double value from the prefix const auto num = extract_longest_double_from_prefix(str); if (num.has_value()) { r_debugstream << __FILE__ << " " << __LINE__ <<": valid case str <" << str << "> value: " << *num << "\n"; return result_validity_t::OK; } switch (num.error()) { case extracting_double_from_prefix_error_types::invalid_input: r_debugstream << __FILE__ << " " << __LINE__ << ": invalid input <" << str << ">\n"; return result_validity_t::ERROR; case extracting_double_from_prefix_error_types::overflow: r_debugstream << __FILE__ << " " << __LINE__ << ": overflow <" << str << ">\n"; return result_validity_t::ERROR; case extracting_double_from_prefix_error_types::value_too_large_to_store_in_type: r_debugstream << __FILE__ << " " << __LINE__ << ": internal error function can not handle value <" << str << ">\n"; return result_validity_t::LIBRARY_ERROR; //If you can setup your compiler properly avoid using default, you need to make sure it will fail compliation if there are unhandled enum values, use your VS settings to fail compilation in such cases } //Why this unhandled cases exist here ? well it is here to be able to identify incorrectly configured compiler, I had not set a static assert or an exception here , as I need it to be working on most compiler with not too many problems return result_validity_t::INVALID_COMPILER_SETTINGS;//This should never happen , but exist to avoid unreachable. } int main() { std::ostringstream debugstream; bool failed_to_treat_a_single_value = false;
//Just iterate on a collection of hard coded values for the example, normally we would be using the test case idiom and returning ok/nok flow for each one of them here for (auto value : {"42","inf","0x10", "0xFFFFFFFFFFFFFFF","NaN","bla bla"}) { result_validity_t test_case_result = treat_single_conversion(debugstream,value); //in reality we need to check and handle each return type correctly, but that is just an example switch (test_case_result) { case result_validity_t::OK :break; case result_validity_t::INVALID_COMPILER_SETTINGS: std::cout << "compiler configuration error found\n"; [[fallthrough]]; case result_validity_t::ERROR:[[fallthrough]]; case result_validity_t::LIBRARY_ERROR: failed_to_treat_a_single_value = true; break; } } if (failed_to_treat_a_single_value) { std::cout << "log is enabled as we had found an error:\n" << debugstream.str() << "\n"; return 1; } return 0; }
זו היא דוגמית קוד המראה את היכולת והפשטות שבמשימוש בstd::expected.
עריכה: בדיעבד לא הסברתי טוב , ה GSL עצמו הוא לא בודק סטטיסטי צריך להפעיל את הבודק הסטטיסטי שיוכל לעבוד עם הקוד איתו עובדים, אני האמת תודות לאדם מאוד טוב לב , יש לי אוסף חוקים בזמן הבדיקה הסטטיסטית שתתריע הכל ממשתנה לא משומש, דרך בעיות המרת טיפוסים לא מפורשת , מצבים ב switch שלא מטופלים ועוד. לצערי קובץ החוקים הזה הוא אינו חופשי לכן אני לא יכול לשתף אותו. וכן לא הראתי פה שימוש ב GSL ו SAL כי הרעיון שלי היה להעביר את std::expected כאבן בניין.
לגבי התגובה של כמה יותר משאבים זה לוקח לעומת הגישה הרגילה של שימוש בשני משתנים נפרדים, מה אני אגיד לכם, 100% שאלה נכונה, אבל מכיוון שאני לא כותב פה מסמך מדיניות, אין תשובה נחרצת לכאן או לכן, יש משקל נוסף לחזרה הזאת בזמן ריצה, אבל בחלק מהפרוייקטים הגישה הזאת יכולה להקטין את זמן הפיתוח והאחזקה ולעשות חיים טיפה יותר קלים למתכנתים, במקומות מסויימים בהם אני כותב קוד למשל אסור להשתמש בזה, כן , אסור להשתמש בכנעט בכל מה שהוא דורש הקצאת זיכרון לאחר הפעלה ודרישות לביצוע בזמן קצוב, אבל האמת ? מרבית המתכנתים והפרוייקטים לא דורשים מגבלות כאלה, לכן להגיד שתמיד אסור או תמיד מותר הו תשובה שאיננה נכונה לכל הפרוייקטים, צריך לבחון את השימוש עם או בלי יחסית למגבלות הפרוייקט והצוות.
2 תגובות:
Your post defends C++ with std::expected and GSL, but it’s misguided. SAL is a verbose, outdated crutch, forcing developers to learn a meta-language to patch C++’s flaws. GSL’s steep learning curve and limitations (no pre/post conditions, poor pointer support) make it a weak solution compared to Rust’s Result and ownership model, which prevent errors at compile time without extra tools. C++’s Concepts and Constraints don’t hold a candle to Rust’s traits and safety guarantees. Companies like Microsoft and AWS are moving to Rust for its reliability and simplicity. Sticking with C++ and SAL/GSL is a losing bet—Rust is the future.
While RUST has it's benifits , it's not a solution for all and for all projects right now, there are entire verified eco-systems that would not move to RUST as it would require emence work for nothing, while smaller systems or projects may move to RUST, this process remind when there was a move from Mainframe based code to linux+Java, that process took ages and emence investments, and it was only done in the mid 2010s
הוסף רשומת תגובה