Side-by-side, interactive cheatsheets for C++ programmers
comparing C++ to other languages. Every example runs live in your browser — no setup, no installation.
Choose your own path by reordering languages
Ruby's C API is one of the most-used C APIs in the world, and that is why you are here. Every gem that is fast is a C or C++ extension — nokogiri is libxml2, pg is libpq, sqlite3 is SQLite — so the common path is not "C++ developer learns Ruby" but "C++ developer writes the fast part of somebody's Ruby program". Four lines of registration and an extconf.rb, and the call site cannot tell.
VALUE is the universal object type, Init_<name> must be extern "C" or mangling hides it, and Ruby checks arity before your function runsrb_thread_call_without_gvl is what makes an extension actually parallel rather than merely faster — and touching any Ruby API while the lock is released is a crashmrb_states per process — where CRuby is what you extend, mruby is what you embedensure, which is how every resource in the standard library is handed outString itself — globally and retroactively, which is both how Rails works and how two gems break each otherThe exit taken by people who are tired of the build, not of the language. Go was designed at a company where a C++ build took the better part of an hour, and almost every omission on this page is downstream of fixing that. A package compiles once and exports a summary, so importing costs the size of an API rather than the size of a codebase — and that one decision is why there are no headers, no cycles, and why generics took twelve years to arrive.
defer, and the obligation moves — a destructor belongs to the type, so every user gets cleanup free; a defer belongs to the call site, so every caller must remember it and the compiler will not remind them&localVariable returned from a function is idiomatic, not a dangling pointer: escape analysis decides stack or heap for you, so the allocation choice you have made your whole career is simply not yours hereconst-correctness at all, no operator overloading, no inheritance, no exceptions, and generics that only avoid writing a function twice — every one argued for rather than merely unimplementedThe language you will end up writing anyway — for the bindings, the build tooling, the data work. Almost every Python surprise a C++ programmer hits comes from one fact: a name is a reference to an object, so assignment never copies, a scope never ends a lifetime, and a signature is never checked. Learn the object model first and the rest of the language stops being surprising.
second = first does not copy — there is no value semantics anywhere in the language and no way to ask for it, which is also why a mutable default argument is shared by every call that omits itwith statement, which is a genuinely good answer — but it guards a block, not an object, so it does not compose through data members the way a destructor does; __del__ is a safety net and never a cleanup mechanism* in the signature makes the trailing booleans keyword-only, turning the classic unreadable call site into a TypeErrornumpy and every scientific library look the way they domultiprocessing, released C code, or the free-threaded buildThe subtractive direction, and the one where you already know the syntax. Almost nothing on this page needs teaching from scratch — what needs naming is the list of things you will reach for out of habit and not find. No destructors, so lifetime becomes a discipline; no overloading, because one name is one symbol; no templates, no containers, no exceptions. Then the shorter and more surprising list: the things C has that C++ never adopted.
goto cleanup — not a code smell in C but the accepted answer, used throughout the Linux kernel, because duplicating the free list at every early return is how you eventually miss oneprivate becomes the opaque pointer, which hides fields from the compiler rather than just the programmer — stronger encapsulation than C++ offers, at the cost of never being stack-allocatable by the callervirtual becomes a struct of function pointers you write out by hand, which is the clearest explanation of what a vtable actually is that you will ever readsizeof('a') is 4 here and 1 there, union type punning is defined in C and undefined in C++, and const int is a variable rather than a constant expressionrestrict, compound literals with real addresses, flexible array members, and designated initializers in any orderThe one language here you will not choose — you will end up next to it. A C++ developer meets Kotlin at the Android NDK boundary, where the engine, the codec or the shared cross-platform core is yours and everything above it is not. So this page is about where your C++ intuitions stop applying in the layer above your .so, and it ends on JNI, which is the part you will actually work on.
second = first aliases rather than copies; data class plus copy() is the closest thing backInt compiles to two different things: a machine int as a local or in an IntArray, and a boxed heap object inside a List<Int> — perhaps twenty times the memory of a std::vector<int>List<Int> and List<String> are one type at runtime, there is no specialization, and reified only rescues it inside inline functionsString cannot be null and a String? cannot be used unchecked, both compile errors, with smart casts so no cast is writtenuse blocks carry cleanup, and finalizers are deprecated and never the answer — the same trade Go, Zig and C# makeUnsatisfiedLinkError in production rather than a compile error — but Kotlin/Native compiles through LLVM with real C interop, which is a better boundary when you can use itEverything you already do by discipline, checked by the compiler instead. RAII is not an alternative to Rust's model — it is Rust's model, extended until the compiler can prove it. Destructors, move semantics, templates, smart pointers and const-correctness all arrive under new names; what is genuinely new is that use-after-move, dangling references, iterator invalidation and data races stop being bugs you avoid and become programs that do not compile.
std::move disappears, because assignment already moves — and the moved-from value is not "valid but unspecified", it is statically poisoned, so use-after-move is a compile error rather than a state you have to remember not to readunique_ptr → Box, shared_ptr → Rc or Arc (Rust splits the atomic refcount from the cheap one, so single-threaded code stops paying for thread safety it never uses), and neither can be null — absence is Optionderive line: a type gets no copy, no comparison and no printing until you name what you want, and Copy is only offered where a bitwise copy is actually correctstd::variant + std::visit becomes an ordinary enum with named variants and exhaustive match; add a case and every incomplete match in the codebase stops compiling, which is the check -Wswitch gives up on the moment a default: appearsunsafe — which is a five-item list, not an off switch, so auditing memory safety means auditing the blocks that are labeledThe one language that can call your C++ classes without a shim. Swift 5.9 shipped direct C++ interoperability — importing headers, methods, templates and operators as a module — which is a different proposition from Rust, Go or C#, all of which meet C++ across a C-shaped boundary. The spine of the language is value semantics: struct copies as it does in C++, and copy-on-write makes that cheap enough to build a whole standard library on.
const& everywhere — passing an array by value costs a retain, not a memcpy, and duplicates only when someone writesshared_ptr with the retains inserted for you: deterministic deinit, no collector, no pauses — and the same retain-cycle problem, solved the same way with weak~Copyable closes the use-after-move hole: a consumed value is a compile error, not a legal read of a "valid but unspecified" state you had to remember to leave sensiblesome versus any spells the static/dynamic dispatch choice in one keyword position, where C++ needs a template and a base-class pointerSendable — but one vendor sets the roadmap, and off Apple platforms the ecosystem is thinThe successor candidate that is arguing with C++ directly. Zig's thesis is one testable question: reading a single line, can you tell whether it allocates and whether it can jump somewhere else? In C++ widgets.push_back(w) may allocate twice, move every element, run constructors and destructors, and throw — none of it visible. In Zig the allocator is a parameter, the failure path is marked try, and cleanup is a defer you wrote.
comptime replaces the preprocessor, templates, constexpr, consteval, if constexpr and static_assert with one mechanism — type is an ordinary value, so a generic type is a function that returns a struct and reflection is a comptime value you loop overerrdefer is the piece C++ lacks — the "undo what I have done so far" half of constructor exception safety, written out instead of emerging from destructor orderingzig cc is a drop-in C and C++ compiler that cross-compiles anywhere from anywhere, libc included — worth adopting even if you never write a line of Zig, and how most C++ programmers meet the projectYou already know the syntax, and that is the trap. C# kept C++'s braces, semicolons, operators and control flow almost unchanged and replaced the machine underneath. The one thing to internalize first is that class and struct are not near-synonyms here — that single distinction decides copy semantics, storage, nullability and equality, and assignment, null, boxing, ref and Span<T> are all downstream of it.
struct copies, class aliases — the same two keywords you read as interchangeable now decide whether b = a duplicates the object or gives it a second nameusing and IDisposable: the guarantee survives, but the obligation moves from the type to every caller, and a finalizer is almost always the wrong toolobject allocates silently, which is what dominates C# performance advice in game codeList<int> really stores ints — but no specialization, no non-type parameters, and the body may only use what the constraint provesSpan<T> is std::span with the lifetime rule enforced: the compiler refuses to let it be boxed, stored in a field or captured, so the dangling-span bug does not compileco_await and leaves the rest to asio or cppcoroLua is not an alternative to C++ — it is already inside your program. Every game engine, Redis, nginx, Neovim, Wireshark. So this page is about the BOUNDARY rather than the syntax: how your classes get bound, who owns an object once Lua can see it, and the one interaction that silently skips your destructors. C++ owns the frame budget; Lua owns the decisions.
lua["damage"] assigned to an int generates the push/pop/convert you would otherwise write by hand and get wrong, and it binds a class with its methods in one calllua_error is longjmp, and longjmp does not run C++ destructors — build Lua as C++ so errors unwind properly, and never let a C++ exception escape into Lua0 and "" are both TRUTHY and an undeclared name is a silent globalGodot's engine IS C++, and since Godot 4 you can add to it without forking it. GDExtension registers a C++ node type the editor lists, inspects and instantiates like a built-in — so this is not a choice between languages but a line drawn through one program. What makes that line cheaper than an embedded scripting language's: both halves share the same object model, so there is no second type system to bridge.
RefCounted is shared_ptr, Object is a raw new with a free() you must call, and Node is freed by its parent; there is no stack allocation and no value semanticsis_instance_valid() is a built-in weak_ptr::expired(), and queue_free() defers destruction to end-of-framevar is std::any, := is auto, and annotating lets the compiler skip Variant dispatch entirelyPacked*Array is a real contiguous buffer with no Variant per element — reaching for one is often the difference between "this needs a C++ node" and "this is fine in GDScript"