1 điểm bởi GN⁺ 18 giờ trước | 2 bình luận | Chia sẻ qua WhatsApp
  • Thư viện chuẩn C++ từ sau C++11 đã lặp đi lặp lại việc chính thức loại bỏ các thiết kế sai lầm hoặc để chúng tồn tại bên cạnh các tính năng thay thế mới, tạo ra một cấu trúc mà lập trình viên phải biết thời điểm của các tầng “không nên dùng”
  • Tầng bị rút lại chính thức gồm các mục như std::auto_ptr, đặc tả ngoại lệ động, giao diện garbage collection của C++11, std::aligned_storage, những thành phần đi kèm các bài báo loại bỏ/xóa bỏ; còn std::function cũng nằm trong một dòng thay thế kéo dài 15 năm được bao quanh bởi std::move_only_function, std::copyable_function, std::function_ref
  • Tầng né tránh không chính thức gồm std::regex chậm chạp, std::async có thể chờ trong destructor và tạo bẫy deadlock, cùng các tính năng như <iostream>, std::list, std::deque, std::vector<bool> vốn vẫn còn trong chuẩn nhưng bị mã production lách qua
  • Vấn đề container mặc định nổi bật ở std::unordered_map, std::map, std::list; trong benchmark cùng workload, P99 của bản C++ ngây thơ là 302.653 cycles, còn bản Rust ngây thơ là 5.177 cycles, chênh lệch 58 lần
  • Lựa chọn ổn định ABI là khác biệt cốt lõi khiến các mặc định sai của C++ bị giữ gần như vĩnh viễn trong std::, trong khi các ngôn ngữ khác giảm sai lầm bằng cách xóa bỏ, edition, hoặc chuyển đổi major version

Điểm khởi đầu: phán quyết “legacy” với std::function

  • Bảng tham chiếu nhanh của Sandor Dargo về std::copyable_function phân loại std::function là “Legacy. Avoid in new code.”
  • std::function được đưa vào từ C++11, còn wrapper thay thế mới nhất là std::copyable_function sẽ vào C++26; điểm được khuyến nghị của tính năng mới không hẳn là “hãy dùng khi cần callable object có thể sao chép”, mà gần hơn với “đừng dùng cái cũ nữa”
  • const operator() của std::function có lỗi const-correctness khi gọi các callable object non-const, và không thể sửa nếu không phá ABI
  • Để xử lý lỗi này, std::move_only_function đi theo luồng P0288R9 của C++23, std::copyable_function theo P2548R6 của C++26, còn std::function_ref theo P0792R14 của C++26

Các tính năng chuẩn đã bị rút lại chính thức

  • std::auto_ptr có ngữ nghĩa copy-as-move làm hỏng generic code và standard container, nên bị đánh dấu deprecated ở C++11 và bị xóa trong C++17 bởi N4190; cùng bài báo đó cũng xóa các adapter của <functional> từ C++98 và std::random_shuffle
  • std::random_shuffle được thay bằng std::shuffle vì nó phụ thuộc vào std::rand và trạng thái toàn cục
  • Đặc tả ngoại lệ động throw(X, Y) bị deprecated ở C++11 và bị xóa ở C++17 bởi P0003R5, còn bí danh throw() bị xóa bởi P1152 trong C++20
  • std::iterator bị deprecated ở C++17 bởi P0174R2, và việc xóa ở C++26 đang được thúc đẩy trong P3365R1; cách thay thế là tự định nghĩa trực tiếp năm typedef
  • std::aligned_storagestd::aligned_union được đưa vào từ C++11 rồi bị deprecated trong C++23 bởi P1413R3; các vấn đề được nêu gồm boilerplate typename ::type, reinterpret_cast, undefined behavior với Len == 0, và thiếu constexpr
  • std::not1, std::not2, unary_negate, binary_negate bị deprecated ở C++17, bị xóa ở C++20, và được thay bằng std::not_fn trong P0005
  • Họ std::declare_reachable của giao diện garbage collection C++11 bị xóa trong C++23 theo P2186R2 vì các implementation lớn chưa từng cung cấp garbage collector thực sự
  • Concepts TS, Modules TS, Coroutines TS, Reflection TS, Executors TS, Networking TS cũng đã trải qua các giai đoạn thiết kế lại, thay thế hoặc trì hoãn trước khi hợp nhất; Reflection chuyển sang P2996, còn Executors chuyển sang luồng sender/receiver của P2300

Các tính năng vẫn còn trong chuẩn nhưng bị né ngoài thực tế

  • std::regex được đưa vào từ C++11, nhưng P1844R1 ghi lại trong hồ sơ ủy ban rằng nó “có hiệu năng rất tệ so với các giải pháp khả dụng khác”; luồng thay thế là CTRE và P1433R0, còn ngoài chuẩn là Boost.Regex, RE2, PCRE2
  • std::async có destructor của future trả về sẽ block đến khi công việc bất đồng bộ hoàn tất, và N3679 ghi lại bẫy deadlock phát sinh từ đó
  • <iostream> chậm, bị ràng buộc với locale, không thread-safe trong formatting, và nổi tiếng vì thông báo lỗi tệ; nhưng ngay cả sau khi có std::format của P0645 trong C++20 và std::print·std::println của P2093 trong C++23, nó vẫn chưa bị deprecated
  • std::list là mục mà Bjarne Stroustrup đã chỉ ra trong keynote GoingNative 2012 rằng ngay cả workload chèn ở giữa thì std::vector vẫn thắng; bài viết tiếp theo Are lists evil? cũng cho câu trả lời gần như là “đúng vậy”
  • std::deque là thành phần mà issue công khai microsoft/STL#147 của Microsoft STL ghi nhận rằng kích thước block do chuẩn ép buộc là quá nhỏ và sẽ cần một cuộc đại tu hiệu năng lớn ở lần ABI break tiếp theo
  • std::valarray được đưa vào từ năm 1998 như một numeric container, nhưng tối ưu expression template chưa từng thành hiện thực; theo cppreference, các implementation dường như không có mã đặc biệt nào vượt trên container thông thường
  • std::vector<bool> được phân tích tiêu biểu trong On vector<bool> của Howard Hinnant; bản thân lưu trữ bit-packed là hữu ích, nhưng việc nó mang tên như một specialization của std::vector tạo ra cạm bẫy khiến generic code hoạt động sai khi T = bool
  • volatile bị deprecated trong phép toán ghép và ở vị trí tham số/giá trị trả về bởi P1152R4 của C++20, rồi được khôi phục một phần bởi P2327R1 trong C++23, và sẽ còn khôi phục thêm trong P2866R0 của C++26

Các container mặc định không thể sửa vì ABI

  • std::unordered_map bị các ràng buộc về bucket và iterator stability trong đặc tả C++11 gần như cấm open addressing; cấu trúc Google SwissTable được nêu là có lợi thế hiệu năng khoảng 3 lần so với std::unordered_map
  • Folly F14, Boost unordered_flat_map, ankerl::unordered_dense cũng là các lựa chọn thay thế theo hướng tương tự; HashMap của Rust dùng bản port SwissTable từ hashbrown làm mặc định trong standard library
  • std::mapstd::set là red-black tree dựa trên node, nên cần cấp phát heap cho từng node và phải đuổi theo con trỏ ở mỗi lần duyệt; Abseil btree_mapBTreeMap của Rust dùng B-tree để tránh các vấn đề đó
  • C++23 đã thêm std::flat_mapstd::flat_set qua P0429R9, nhưng không thể thay đổi thiết kế mặc định của chính std::unordered_map, std::map, std::list
  • multi-symbol order book benchmark so sánh std::unordered_map + std::map + std::list của C++ với HashMap + BTreeMap + VecDeque của Rust trên cùng workload, cùng seed, cùng core cô lập
Cài đặt P99 cycles
C++ naive (unordered_map + map + list) 302,653
C++ step 1 (flat_hash_map + map + deque) 9,951
C++ step 2 (flat_hash_map + btree_map + deque) 9,114
C++ step 3 (flat_hash_map + btree_map + vector) 4,268
Rust naive (HashMap + BTreeMap + VecDeque) 5,177
  • Chỉ riêng việc chuyển từ std::list sang std::vector cho hiệu quả khoảng 70 lần, chuyển từ std::unordered_map sang flat_hash_map cho 3–5 lần, còn chuyển từ std::map sang btree_map cho 1,09 lần và nằm trong mức nhiễu
  • Trọng tâm của so sánh không phải là bản thân ngôn ngữ Rust nhanh hơn C++ 58 lần, mà là standard library của Rust đã chọn đúng mặc định, còn standard library của C++ không thể sửa ba mặc định đó vì ABI

Vấn đề Vasa và sự tích tụ tính năng

  • Tài liệu WG21 năm 2018 P0977R0 “Remember the Vasa!” của Bjarne Stroustrup dùng vụ chìm chiến hạm Thụy Điển Vasa năm 1628 làm phép ẩn dụ, và chẩn đoán rằng ủy ban có “khoảng 150 đầu bếp” nhưng không xử lý đầy đủ tác động của từng tính năng riêng lẻ lên toàn bộ hệ thống
  • std::simd được nêu trong std::simd Is a Solution to the Wrong Problem như một ví dụ tiêu biểu của cùng mẫu hình; đây là tính năng do Matthias Kretz khởi đầu từ thư viện Vc, rồi đi qua P0214, Parallelism TS 2, P1928 trước khi vào C++26
  • Khi std::simd vào chuẩn, bên ngoài chuẩn đã có Google Highway, ISPC, EVE, xsimd, SIMDe, còn auto-vectorizer của GCC và Clang cũng đã cải thiện; bài viết cho rằng vòng lặp scalar với -O3 -march=native còn cho kết quả tốt hơn std::simd
  • std::simd có các vấn đề như thời gian biên dịch chậm hơn 10 lần so với mã scalar tương đương, chạy chậm hơn cả auto-vectorizer mà nó định thay thế, và không biểu diễn được vector scalable-width của ARM SVE cùng runtime dispatch
  • Ba implementation libstdc++, libc++, MSVC STL đều được duy trì bởi các nhóm kỹ sư quy mô một chữ số; mỗi tính năng chuẩn mới lại làm tăng test matrix, bug tuân thủ chuẩn, tương tác giữa các tính năng, và số issue trong bug tracker mà người bảo trì tiếp theo sẽ phải kế thừa
  • std::regex mang các vấn đề đã biết suốt 15 năm, std::deque có issue cần thiết kế lại, còn C++20 modules được mô tả là sau 6 năm chuẩn hóa vẫn chưa hoạt động trơn tru trên cả ba implementation
  • Tri thức vận hành thực tế của chuẩn C++ hiện đại vì thế tập trung vào một số ít chuyên gia toàn thời gian, những người đã học được thời điểm của các tầng sai lầm, các lối vòng qua bằng thư viện bên thứ ba, sự khác biệt giữa ba implementation của standard library, và chênh lệch giữa lý thuyết với thực tiễn

Khác biệt với các ngôn ngữ khác: không phải có sai hay không, mà là tỷ lệ giữ lại

  • Python đã xóa hơn 20 module standard library qua PEP 594, xóa distutils trong Python 3.12 qua PEP 632, còn PEP 387 nêu rõ quyền rút ngắn chu kỳ deprecated đối với các tính năng nguy hiểm hoặc hỏng
  • Java đã xử lý Applet API theo lộ trình 8 năm: deprecated trong Java 9, marked for removal trong Java 17, rồi xóa thực sự trong JEP 504; Nashorn cũng bị xóa trong Java 15 bởi JEP 372
  • Java SecurityManager bị deprecated for removal bởi JEP 411 và bị vô hiệu hóa vĩnh viễn bởi JEP 486, còn JEP 398 nói về lộ trình Applet API bị loại bỏ
  • Rust cho phép chọn edition 2015, 2018, 2021, 2024 theo từng crate trong Cargo.toml; mem::uninitialized được thay bằng MaybeUninit, std::error::Error::description được thay bằng source, còn macro try! được thay bằng toán tử ?
  • C# chấp nhận các đợt chuyển major version từ .NET Framework sang .NET Core, loại bỏ BinaryFormatter, AppDomains, Remoting, Code Access Security, WCF server, WebForms
  • JavaScript hầu như không xóa vì ràng buộc tương thích web, nhưng cancelable promises đã bị rút ở Stage 1 và SIMD.js bị bỏ để chuyển sang WebAssembly SIMD; Go vì cam kết tương thích Go 1 nên chọn chỉ đánh dấu io/ioutil là deprecated
  • Điểm khác biệt của C++ không phải là có mắc sai lầm hay không, mà là tỷ lệ giữ lại quá cao đối với các mục như std::regex, std::unordered_map, std::vector<bool>, std::valarray, và lỗi const-correctness của std::function

Sự bảo tồn vĩnh viễn do ổn định ABI tạo ra

  • P1863R1 “ABI - Now or Never” là luồng tranh luận hỏi liệu có nên chấp nhận ABI break trong C++23 hay chọn ổn định ABI vĩnh viễn, và ủy ban trên thực tế đã chọn ổn định ABI vĩnh viễn
  • Chính lựa chọn này khiến việc sửa std::regex, chuyển std::unordered_map sang open-addressing, hay thay đổi cấu trúc của std::list, std::map, std::deque trở nên khó khăn
  • ABI của standard library C++ bị dynamic linker cưỡng chế, nên object được biên dịch bằng một bản phát hành libstdc++ phải liên kết được với object của bản phát hành khác; vì vậy những chi tiết như layout của std::string hay cấu hình std::regex_traits bị đóng đinh vào binary được phân phối
  • Các ràng buộc này được cụ thể hóa trong các tài liệu như libstdc++ ABI policyItanium C++ ABI
  • Người dùng Python chọn python==3.12, người dùng Rust chọn edition trong Cargo.toml, người dùng Java chọn phiên bản JDK, người dùng C# chọn TFM như net6.0 hay net8.0, nhưng C++ không có Cargo.toml cho std::
  • -std=c++26 chỉ chọn header và luật ngôn ngữ nào sẽ dùng, chứ không cung cấp một std::string khác hay một std::unordered_map được thiết kế lại
  • Vì thế, standard library C++ được đưa vào production năm 2026 vẫn tiếp tục mang theo, bằng chính thiết kế và tính cưỡng chế của nó, những mặc định sai mà ủy ban đã chấp nhận từ năm 1998
  • Các codebase C++ hiện đại ở tier-one trading firm, search engine, browser phụ thuộc nặng vào các thư viện phi chuẩn như Boost, Abseil, Folly, EASTL, Chromium //base, container tự viết, custom allocator, CTRE, Outcome, các thư viện coroutine

2 bình luận

 
dieafterwork 3 giờ trước

Bản gốc khá dài và nặng, đọc đến cuối thì đúng là có cảm giác tác giả khá thiên về Rust.
Dù vậy tôi cũng biết thêm được khá nhiều thông tin trước đây chưa biết. Cảm ơn vì bài viết hay.

 
Ý kiến trên Lobste.rs
  • Nếu nhớ lại xem hệ sinh thái Rust có từng có mức độ churn tương tự hay không, thì có vẻ chỉ có vài vụ lớn
    Trong thời kỳ Leakpocalypse, người ta đi đến kết luận rằng không thể dựa vào việc destructor Drop sẽ luôn được chạy để duy trì các bất biến an toàn, và thay đổi API thực tế thì hầu như không có, ngoài việc loại bỏ std::thread::scoped. Sau đó đã có một giải pháp thay thế làm được điều tương tự một cách sound
    std::mem::uninitialized đã bị deprecated và giờ bị xem là unsound. Các kiểu Range hiện có sẽ dần được thay thế bằng các kiểu mới gần như giống hệt để sửa vài vấn đề API tương đối nhỏ. std::error::Error::description bị deprecated vì đa số kiểu lỗi không muốn lưu trữ chuỗi, và đã có một phương án thay thế trực tiếp là implementation của Display
    Nghĩ lại thì việc std đã ổn định suốt 11 năm là khá đáng ngạc nhiên, và phần còn lại của std vẫn tồn tại, vẫn hoạt động, và 98% vẫn được xem là Rust mang tính thành ngữ. Trong khi đó thư viện chuẩn C++ có vẻ đang ở vào một vị thế nguy hiểm: quá dễ dãi khi thêm tính năng, nhưng lại bảo thủ đến mức đáng ngạc nhiên với việc deprecate trong gần như mọi tình huống

    • Bằng cách nào đó tôi hoàn toàn không biết gì về Leakpocalypse: faultlore (2015)
    • Tôi cũng nhớ tới vấn đề trait Iterator tự mượn chính nội dung của nó. Đây là một vấn đề dai dẳng cứ liên tục xuất hiện trong các cuộc thảo luận về Rust dưới dạng “tại sao cái này lại không dùng được và phải lách đường vòng”
      Tương tự, việc f32f64 không implement Cmp mà thay vào đó có phương thức f32::total_cmp cũng là một điểm phiền toái mà các kỹ sư mới thường hay vấp phải, khiến người ta lại phải thở dài rồi giải thích bối cảnh
      Cơ chế định dạng panic cũng không hay lắm, và có nhiều bài blog nói rằng panic handler mặc định dùng formatting, rất khó tắt đi, và vì vậy làm tăng kích thước file thực thi khá nhiều
  • Cá nhân tôi nghĩ thiết kế cũ kỹ của thư viện chuẩn làm giảm mạnh mức độ phổ biến và tính dễ dùng của C++
    Nhiều vấn đề thường bị đổ cho bản thân ngôn ngữ thì thực ra đáng lẽ nên nhắm vào thư viện chuẩn hơn
    Ví dụ, câu “C++ biên dịch chậm” là không chính xác. Không phải cứ dùng tính năng C++ là chậm một cách bản chất, mà thứ làm nó chậm là thư viện chuẩn với tình trạng header phình to, phụ thuộc chằng chịt, và lạm dụng template ngay cả cho các abstraction đơn giản
    Câu “C++ không an toàn” cũng đúng một phần, nhưng thiết kế thư viện chuẩn còn làm tình hình tệ hơn. Không có lý do gì mà các pattern an toàn hơn trong thiết kế API của Rust lại không thể được áp dụng vào một thư viện chuẩn mới. Tất nhiên, một trong những ưu điểm lớn của C++ là khả năng tương thích ngược, nên đây là một vấn đề cực kỳ phức tạp

    • Trong một số trường hợp thì đúng. Có thể làm cho vec[idx] ném exception hoặc abort thay vì truy cập ngoài phạm vi/hành vi không xác định. Nhưng cũng có nhiều trường hợp mà do khác biệt ngôn ngữ, việc tạo ra API an toàn trong C++ khó hơn rất nhiều
      Rust về cơ bản có destructive move, còn C++ thì không. Vì vậy API smart pointer buộc phải hoặc là unsafe, hoặc ít nhất là dễ gây bất ngờ và crash, ví dụ như abort chương trình khi truy cập smart pointer sau khi đã move khỏi nó
      Rust có lifetime annotation còn C++ thì không. Vì vậy Rust có thể ngăn những thứ như invalidation của iterator trong thiết kế API iterator, còn C++ thì về thực chất là rất khó. Rust có pattern matching nên các API như Option có thể cung cấp cách “kiểm tra rồi dùng ngay” một cách ergonomically. C++ cũng có thể cung cấp một phiên bản std::option không tạo ra UB khi truy cập giá trị rỗng, nhưng sẽ bất tiện hơn nhiều so với C++ hiện tại hay Rust. Toán tử ? của Rust cũng giúp rất nhiều ở đây
      Tôi biết C++ có thể vá víu thứ gì đó giống pattern matching bằng overload set như std::variant, nhưng theo tôi nó khó dùng hơn nhiều và dễ mắc lỗi hơn
    • Tôi nghĩ C cũng vậy. Nhiều vấn đề của C xuất phát từ việc stdlib quá tệ
      Chỉ cần có một thư viện hiện đại với thư viện chuỗi và mảng, vài container generic, cùng hỗ trợ native cho allocator thì C đã có thể trở nên ergonomic và dễ dùng hơn rất nhiều. Tất nhiên, một số khiếm khuyết của ngôn ngữ sẽ không biến mất chỉ bằng cách thay thư viện, nhưng như vậy vẫn có thể đi được khá xa
      Nhìn vào các codebase C hiện đại, người ta dùng rất rộng rãi các thư viện tùy chỉnh cho allocator, chuỗi, vector, hash table và thao tác file system, và nếu bạn đã có kinh nghiệm với C hay quản lý tài nguyên thủ công thì cũng không quá khó để đi theo hướng đó
    • Ở công ty, chúng tôi dùng một implementation slice<T, N> có thể biểu diễn “con trỏ trỏ tới chính xác N byte” hoặc “con trỏ trỏ tới một số byte bất kỳ”
      Nó có head(n), tail(n), slice(start, end) và toán tử chỉ mục, tất cả đều có kiểm tra biên
      Làm việc với những abstraction như thế thực sự rất thích, nhưng để có được một ngôn ngữ hiện đại và phần nào an toàn thì về cơ bản bạn phải port thư viện chuẩn của Rust và Zig sang C++. Dù vậy, cuối cùng vẫn đáng công bỏ ra
    • Nếu dùng ít template hơn cho các abstraction đơn giản thì có phải sẽ mất hiệu năng không?
  • Nếu đã viết kiểu bài này thì làm ơn hãy tự viết lấy. Có thể danh sách là do tác giả tự làm, nhưng chuyện đưa nó vào LLM rồi đẩy kết quả lên web cho người khác đọc là quá bất lịch sự. Nếu tôi còn phải đọc thêm một câu kiểu “một kỹ sư đang làm việc” được dạy phải tránh “tính năng X” ngay từ “ngày đầu tiên” nữa thì chắc tôi phát điên mất
    Điều đáng xấu hổ là ở đây thực ra có rất nhiều điều để nói, nhưng rốt cuộc lại chẳng nói gì cả. Hẳn phải có lý do tác giả viết bài này, và tôi chỉ mong nói ra lý do đó. Chắc hẳn có phần nào đó của C++ khiến tác giả bực bội, và có tính năng nào đó đã làm tác giả bối rối. Những tính năng này tệ không chỉ vì thất bại thiết kế một cách khách quan, mà còn vì ảnh hưởng của chúng lên chúng ta
    Kiểu như bạn đã từng dùng std::iterator rồi bị người ta chỉnh ngay trên Slack chưa, hay có bao giờ bạn không dùng reinterpret_cast chỉ vì nó dài 16 ký tự và làm xấu format của dòng mã không; nếu những câu chuyện như thế được đăng lên Lobsters thì sẽ hay hơn nhiều. Còn nếu không có, thì đừng cố bịa ra, và cũng đừng bắt GPU tạo ra cùng một câu đến 10 lần bằng phép nhân ma trận. Chỉ cần chú thích vào những chỗ đáng bình luận, còn lại dùng bảng và bullet point là được

    • Bài này không có cảm giác như được viết bằng LLM
  • Tôi đã dùng C++ 20 năm và đến giờ vẫn dùng, nhưng khá đồng ý với bài viết này. Điều thực sự tuyệt vời khi dùng Rust dạo gần đây không phải là độ an toàn bộ nhớ mà là thư viện chuẩn và hệ sinh thái gói cực tốt
    Một ví dụ tiêu biểu là thư viện ranges. Đã 6 năm kể từ khi được chuẩn hóa nhưng các thư viện chuẩn lớn vẫn chưa triển khai đầy đủ, và ngay cả khi có thì cũng chỉ có vài combinator. Phía tương ứng trong Rust là các phương thức Iterator, có 76 cái, và chỉ cần cargo add một lần là trait itertools bổ sung thêm 130 cái nữa
    Một thứ tôi thực sự nhớ nữa là pattern matching. Nó có thể làm cho các kiểu union như std::variant trở nên ergonomic. Đề xuất vẫn đang được thảo luận nhưng đến C++26 vẫn chưa có, điều đó khá đáng tiếc. Trong khi đó contracts và executors lại được đưa vào, mà thành thật thì tôi chưa từng thấy ai quanh mình yêu cầu chúng

    • Một trong những vấn đề của C++ là không có tiêu chí chính thức và được ghi thành tài liệu về việc tính năng nào nên là tính năng ngôn ngữ, và tính năng nào nên là tính năng của thư viện chuẩn
      Nói chung, tiêu chí tôi dùng là thế này. Nếu một tính năng hỗ trợ trường hợp sử dụng mong muốn và không thể biểu đạt bằng thư viện chuẩn, thì nó nên được đưa vào ngôn ngữ. Nếu có thể, nên tách nó thành những thành phần độc lập tối thiểu để tính năng mong muốn đó cũng có thể được dùng cho mục đích khác
      Những tính năng được dùng trong gần như mọi codebase nên nằm trong thư viện chuẩn. Nếu một kiểu dữ liệu thường được dùng làm giao diện giữa các thư viện thì nó nên nằm trong thư viện chuẩn. Chúng ta không muốn mọi thư viện đều tự định nghĩa kiểu tuple hay chuỗi của riêng mình. Với vế đầu thì C++ về cơ bản từng như vậy cho đến trước C++11, còn với vế sau thì do std::string là một disaster nên đến giờ vẫn vậy. Điều này cũng áp dụng cho các kiểu giao diện, và C++ ngày nay chủ yếu xử lý bằng concepts
      Phần còn lại nên nằm trong các thư viện mô-đun có thể tái sử dụng. Rust làm khá tốt việc có một tập hợp thư viện ngoài ổn định và được tin dùng, nên áp lực kiểu “mọi game viết bằng Rust đều cần cấu trúc dữ liệu này, hãy đưa nó vào thư viện chuẩn” nhỏ hơn nhiều. Người làm game chỉ cần mang crate cần thiết vào là được. C++ chưa bao giờ thực sự chấp nhận ý tưởng “một gói tốt để khuyến nghị cho vấn đề mà nhiều người, nhưng không phải đa số, gặp phải”
  • Điều đáng lo là trong số những thứ đang được bổ sung hiện nay, thứ nào rồi sẽ lại bị rút về sau. Contracts vừa mới vào C++26 mà đã có người chỉ ra các lỗi thiết kế nghiêm trọng
    Tôi không muốn công kích chung chung cái gọi là “thiết kế bởi ủy ban”. Tôi nghĩ những tổ chức như vậy phục vụ mục đích quan trọng và có thế mạnh riêng. Chỉ là thế mạnh đó không nằm ở việc thiết kế các tính năng hoàn toàn mới trên một bãi đất trống
    WG21 và WG14 thực sự tỏa sáng khi lấy những tính năng mà không gian thiết kế đã được khám phá phần nào, tốt nhất là đã có nhiều triển khai sẵn, rồi biến chúng thành tính năng chuẩn mà phần lớn người dùng và bên triển khai đều có thể chấp nhận. std::embed là một ví dụ như vậy
    Ngược lại, như phần mở rộng GC, std::memory_order_consume, hay modules của C++20 được nhắc trong bài, nếu chuẩn hóa trước cả khi có ai đó triển khai cho ra hồn thì mọi thứ thường trở nên rất tệ

    • C++ và Haskell đều là ngôn ngữ do ủy ban thiết kế, nhưng hai ngôn ngữ này gần như đối lập nhau. Mỗi khi muốn nghĩ rằng “$X được ủy ban thiết kế” hàm ý điều gì đó về $X, tôi lại nhớ đến điều này
  • Trước đây tôi đã khá sốc khi nhận ra C++ không quản lý phiên bản standard library. Tôi không ngờ bài này lại chỉ đúng vào điểm đó
    Điểm bài viết nhắc rằng Go cũng bảo thủ theo cách tương tự về forward compatibility cũng khá thú vị. Nhưng Go dường như cũng bảo thủ tương tự với cả các tính năng được bổ sung, nên có vẻ đã tránh được phần lớn vấn đề của C++. Việc không có ABI ổn định chắc cũng giúp ích
    Trong số các thư viện phổ biến mà tôi biết, chỉ có libcamera là công khai phơi bày ABI C++, và điều đó khá phiền. Theo kinh nghiệm của tôi, ngay cả thư viện C++ cũng thường xuất symbol bằng C ABI, và điều đó cũng giúp việc tương tác với các ngôn ngữ khác dễ hơn. Có thể tôi đã bỏ lỡ xu hướng nào đó
    Và chẳng phải vẫn có những quirks trong tương thích ABI giữa Clang và MSVC sao? Tôi nhớ Conan từng công khai discouraging hoặc cấm trộn lẫn compiler, nên tôi thấy khó hiểu vì sao ủy ban C++ lại cố giữ ổn định ABI đến vậy

    • Điều đó không hoàn toàn đúng. C++ chỉ là không quản lý phiên bản standard library độc lập với ngôn ngữ
      Ở đây có hai thứ liên quan chặt chẽ với nhau: đặc tả standard library và phần triển khai. Đặc tả là cho tổ hợp ngôn ngữ + thư viện hoàn chỉnh, còn phần triển khai thường cố hỗ trợ ít nhất một hoặc nhiều phiên bản đặc tả
      Có nhiều thư viện phơi bày giao diện C++, trong đó có cả những thư viện rất lớn như Qt
      Vấn đề là máy trừu tượng của C++ không định nghĩa quá trình liên kết. Vì vậy nó không thể định nghĩa cách dynamic library hoạt động. Liên kết động C++ trên hệ thống UNIX đi theo mô hình của C. Nó giả vờ là dynamic linking rồi đẩy vấn đề sang cho loader. Điều này dẫn đến những thứ kinh khủng như copy relocation. Windows có khái niệm nguyên tắc hơn nhiều về việc shared library là gì, nhưng vì thế một số idiom của thư viện C++ trên UNIX lại không thể hoạt động trên Windows
      Shared library đặc biệt có vấn đề với các tính năng như template của C++. Nếu muốn instantiate template với kiểu do người dùng định nghĩa, thì toàn bộ định nghĩa phải nằm trong header, vì compiler không thể nhìn qua ranh giới compilation unit. Trong shared library, cùng một đoạn mã sẽ được instantiate ở nhiều nơi. Nếu chương trình và thư viện cùng instantiate một template với cùng tham số thì cả hai sẽ có bản sao, và linker cùng loader phải đảm bảo rằng trong chương trình cuối cùng sau khi nạp chỉ dùng một bản
      So với Swift thì Swift nói rõ rằng “shared library có tồn tại, và ngôn ngữ phơi bày các cấu trúc cấp ngôn ngữ để biểu diễn điều đó”. Nếu bạn muốn phơi bày generic qua ranh giới shared library thì vẫn làm được, nhưng với mọi caller bên ngoài nó sẽ bị hạ xuống thành phiên bản dynamic dispatch. Trong C++ cũng có thể tự làm bằng tay. Bạn tạo một template phiên bản tổng quát dùng type-erasure wrapper, rồi dùng tường minh các instantiation cụ thể khác. Nhưng việc này khó và thủ công. Trong Swift thì đơn giản là “qua ranh giới shared library thì nó sẽ như vậy”
      Việc ẩn kiểu cũng tương tự. C++ dùng mẫu pImpl để tạo public interface phơi bày hành vi qua ranh giới thư viện nhưng ẩn phần triển khai. Swift có một máy trừu tượng biết ranh giới thư viện nằm ở đâu, và nói rằng “kích thước của một kiểu không được đánh dấu là ABI-stable không phải là hằng số tại thời điểm biên dịch khi đi qua ranh giới shared library”
      Đây cũng là một dạng khác của việc tiêu chuẩn phủ nhận thực tế. Gần như mọi codebase C++ không tầm thường mà tôi từng làm đều được biên dịch với -fno-rtti -fno-exceptions hoặc các tùy chọn tương đương của CL.EXE. Tiêu chuẩn không thừa nhận đây là một khả năng. Phần lớn hàm trong standard library vẫn giả định dùng exception để báo lỗi, nên nếu biên dịch với -fno-exception thì nó sẽ chỉ gọi abort. Vì thế các thành phần standard library có cấp phát bộ nhớ động trở nên không dùng được trong môi trường nhúng. std::vector<T>::push_back có thể làm chương trình crash
      Đoạn trong bài nói rằng “ủy ban không chỉ không thể loại bỏ các tính năng tệ, mà còn tiếp tục thêm tính năng mới mà kỹ sư thực tế không hề yêu cầu” giống 100% với cách contracts xuất hiện. Verus cho thấy một hệ thống contracts tốt có thể làm được gì trong một ngôn ngữ nhắm tới môi trường giống C++. Contracts P2900 là tổ hợp của các yêu cầu mâu thuẫn nhau, nên nó làm mọi vấn đề mà contracts lẽ ra có thể giải quyết trở nên tệ hơn
      Tôi không nghĩ kết luận rằng “kỹ sư C++” được trả lương cao hơn rất nhiều so với “kỹ sư biết lập trình” là đúng. Thực tế là chẳng ai viết code đúng nguyên bản theo chuẩn C++, mà ai cũng viết theo cái tập con-trong-tập cha mở rộng nội bộ họ ưa thích
    • go vet ở đây cũng có giá trị. Vì nó cung cấp nâng cấp tự động để cải thiện API
  • Từ năm ngoái tôi gần như đã bỏ C++, ban đầu chuyển sang Kotlin rồi sau đó là Swift. Ở công ty tôi vẫn phải bảo trì C++, nhưng mã mới viết thì gọn gàng, súc tích và an toàn hơn nhiều. Có đánh đổi về kích thước mã và có thể cả hiệu năng, nhưng hoàn toàn xứng đáng

  • Tôi nhớ nghĩa của vòng lặp for trong Go đã thay đổi theo cách phá vỡ tương thích ngược, nên ban đầu tôi nghĩ câu này là sai: https://go.dev/blog/loopvar-preview
    Nhưng hóa ra Go cũng dùng cách giống Rust editions ở điểm này. Phải khai báo phiên bản Go là 1.22 trở lên thì ngữ nghĩa mới thay đổi. Có lẽ io/ioutil cũng có thể bị loại bỏ theo cách này, nhưng chắc chưa đáng để phá mã vượt qua cả ranh giới edition

  • Nếu C++ không thực sự thử những ý tưởng tồi này và chứng minh rằng chúng là ý tưởng tồi, thì có lẽ Rust đã không thể tồn tại ở hình dạng hiện tại. Xin gửi lời cảm ơn thật lớn!

  • Tôi quan tâm đến một thư viện thay thế standard library kiểu Rust cho C++. Tôi biết rpp nhắm đến mục tiêu đó: https://github.com/TheNumbat/rpp
    Có lựa chọn nào khác không? Ý tôi không phải các bản triển khai khác của stdlib C++ như EASTL, mà là các thư viện đi gần với Rust hơn. Tôi biết một số thứ như std::initializer_list đã bị đóng đinh vào cú pháp, nhưng ngoài ra thì mọi thứ đều có thể thay đổi