PONYλM2Modula-2

C++.CodeCompared.To/Swift

An interactive executable cheatsheet comparing C++ and Swift

C++23 (GCC) Swift 6.3
Hello World & the Toolchain
Hello, World
One line, no includes and no entry point — and unlike a scripting language, this compiles to a native binary with no runtime interpreter.
#include <iostream> int main() { std::cout << "Hello, World!" << std::endl; return 0; }
print("Hello, World!")
A Swift file at top level executes its statements in order, so main is synthesized rather than written. The standard library is implicitly available, so there is nothing to import for ordinary work — import Foundation is only needed for the older Objective-C-derived layer. print appends a newline and renders any type through its CustomStringConvertible conformance, so there is no stream-operator chain and no format string to mismatch.
One compiler, one package manager, one formatter
Swift made the same bet Go and Rust made — one answer per question, in the toolchain — with a caveat neither of them has.
// Every C++ project chooses its own answers: // // Build — CMake, Meson, Bazel, Make, MSBuild // Packages — vcpkg, Conan, submodules, system, vendored // Format — clang-format with ~100 options to argue about // Test — Catch2, GoogleTest, doctest, Boost.Test // Compiler — GCC, Clang, MSVC, each with its own extensions // // The upside is that no single vendor controls it and it runs // everywhere; the downside is five decisions before the first line. #include <iostream> int main() { std::cout << "portable, and assembled by hand" << std::endl; return 0; }
// Swift ships one of each, in the toolchain: // // Build — swift build (SwiftPM), Package.swift written in SWIFT // Packages — the same tool; dependencies are git URLs, pinned // Format — swift-format // Test — swift test, with the Testing framework built in // Compiler — one implementation, from Apple // // The upside is that none of it is a decision; the downside is that // one vendor sets the direction, and platform support outside Apple's // own is real but second-class. print("batteries included, one vendor")
The manifest, Package.swift, is written in Swift itself, so there is no build DSL to learn. What is genuinely different from Go and Rust is governance: Swift has one implementation and Apple sets the roadmap, so a feature you need arrives when Apple ships it. Linux and Windows support are real and used in production, but Apple platforms get the tooling, the debugger integration and the frameworks first. That is the strategic consideration to weigh, quite apart from anything about the language.
Value Types & Copy-on-Write
struct copies, class references
The same two keywords you read as near-synonyms in C++ decide copy semantics here, exactly as they do in C#.
#include <iostream> // Both are value types; they differ only in default access. struct PointStruct { int x; int y; }; class PointClass { public: int x; int y; }; int main() { PointStruct first{1, 2}; PointStruct second = first; // a copy second.x = 99; PointClass third{1, 2}; PointClass fourth = third; // also a copy fourth.x = 99; std::cout << first.x << " " << third.x << std::endl; return 0; }
struct PointStruct { var x: Int; var y: Int } class PointClass { var x: Int; var y: Int init(x: Int, y: Int) { self.x = x; self.y = y } } var first = PointStruct(x: 1, y: 2) var second = first // a COPY — struct is a value type second.x = 99 let third = PointClass(x: 1, y: 2) let fourth = third // a REFERENCE — two names, one object fourth.x = 99 print(first.x, third.x)
A struct is a value type — it copies on assignment, cannot be nil, and gets a memberwise initializer for free. A class is a reference type, heap-allocated and reference-counted. So the C++ column prints 1 1 and the Swift column prints 1 99. What differs from C# is the default: Swift's standard library is built on structs — Array, String, Dictionary and every numeric type are all value types — and the guidance is to reach for struct first and class only when you need identity or inheritance. The next row is why that is affordable.
Copy-on-write makes value semantics cheap
This is the piece that makes Swift's value-semantics-everywhere design practical, and it removes the reason C++ needs const T& as a universal convention.
#include <iostream> #include <vector> // By value COPIES the whole buffer, every call — which is why the // universal C++ convention is to write `const std::vector<int>&`. void observe(std::vector<int> byValue) { std::cout << byValue.size() << " "; } int main() { std::vector<int> readings(1000, 7); observe(readings); // 1000 elements copied here std::vector<int> alias = readings; // and eagerly copied again NOW readings[0] = 99; std::cout << alias[0] << " " << readings[0] << " " << readings.size() << std::endl; return 0; }
func observe(_ values: [Int]) { // by value, semantically print(values.count, terminator: " ") } var readings = Array(repeating: 7, count: 1000) // No buffer is copied here: the array shares storage until someone // WRITES, at which point the writer gets its own copy. observe(readings) let alias = readings readings[0] = 99 // THIS is where the copy happens print(alias[0], readings[0], readings.count)
Every value type with heap storage — Array, String, Dictionary, Set — shares its buffer on copy and duplicates only on the first write, using the reference count to detect uniqueness. So passing an array by value costs a retain, not a memcpy, and you never write const& to avoid a copy that is not happening. The consequences are worth internalizing: the copy cost moves from where you can see it (the call) to where you cannot (a mutation), and a value type behaves as if it were deeply copied, so there is no aliasing to reason about at all.
mutating is const-correctness inverted
Swift annotates the opposite half of the pair from C++, and the difference in which case is the default matters more than it sounds.
#include <iostream> struct Counter { int total = 0; void increment(int by) { total += by; } // non-const by default int getTotal() const { return total; } // const must be written }; int main() { Counter counter; counter.increment(5); const Counter frozen = counter; // frozen.increment(1); // error: increment is not const std::cout << counter.getTotal() << " " << frozen.getTotal() << std::endl; return 0; }
struct Counter { var total = 0 // The MUTATING case is the one that must be marked. mutating func increment(by amount: Int) { total += amount } func getTotal() -> Int { total } } var counter = Counter() counter.increment(by: 5) let frozen = counter // frozen.increment(by: 1) // error: `frozen` is a `let` constant print(counter.getTotal(), frozen.getTotal())
C++ makes you write const on the methods that do not mutate, which is a discipline that erodes the moment one function forgets. Swift makes you write mutating on the ones that do, so the annotated case is the rarer one and therefore actually gets written and noticed — the same inversion Rust made with mut. Enforcement comes from the binding rather than the reference: a let value type cannot have a mutating method called on it at all. Note also the argument label by:, which is part of the method's name — increment(by:) — and is why Swift call sites read the way they do.
ARC & Lifetime
ARC is shared_ptr with the retains inserted for you
ARC is reference counting, not tracing garbage collection — so cleanup is deterministic, exactly as with shared_ptr.
#include <iostream> #include <memory> #include <string> struct Session { std::string name; explicit Session(std::string value) : name(std::move(value)) { std::cout << "open " << name << std::endl; } ~Session() { std::cout << "close " << name << std::endl; } }; int main() { { auto first = std::make_shared<Session>("audit"); auto second = first; // refcount 2, atomic std::cout << "count " << first.use_count() << " " << first->name << std::endl; } // freed here, deterministically std::cout << "after scope" << std::endl; return 0; }
class Session { let name: String init(name: String) { self.name = name; print("open \(name)") } deinit { print("close \(name)") } } do { let first = Session(name: "audit") let second = first // retain — inserted by the compiler print("count 2", second.name) } // released here, deterministically print("after scope")
The compiler inserts the retain and release calls, so you never write them, and deinit runs at a predictable point when the last reference goes away. That is a meaningfully different bargain from C# or Go: no collector, no pause, and a destructor you can rely on for closing files and sockets. The costs are the ones shared_ptr has too — an atomic refcount operation on every copy, which shows up in hot loops — plus the one in the next row, which C++ has as well and Swift cannot solve either.
Retain cycles, and two ways out
Reference counting cannot collect a cycle, so this is a problem Swift inherits wholesale from shared_ptr — and the reader already knows the shape of it.
#include <iostream> #include <memory> struct Child; struct Parent { std::shared_ptr<Child> child; ~Parent() { std::cout << "parent gone" << std::endl; } }; struct Child { // weak_ptr breaks the cycle. shared_ptr here would leak both. std::weak_ptr<Parent> parent; ~Child() { std::cout << "child gone" << std::endl; } }; int main() { { auto parent = std::make_shared<Parent>(); auto child = std::make_shared<Child>(); parent->child = child; child->parent = parent; } std::cout << "scope over" << std::endl; return 0; }
class Child { // weak is optional and becomes nil; unowned is non-optional and // traps if accessed after the target is gone. weak var parent: Parent? deinit { print("child gone") } } class Parent { var child: Child? deinit { print("parent gone") } } do { let parent = Parent() let child = Child() parent.child = child child.parent = parent // a strong ref here would leak both } print("scope over")
The correspondence is direct: weak is std::weak_ptr, and Swift adds unowned, which is a non-optional non-owning reference that traps if the target is gone rather than being checkable. Use weak when the target may legitimately outlive nothing, and unowned when the reference logically cannot outlive its target — a child pointing at a parent that owns it. The place this bites in practice is closures, which capture strongly by default: a closure stored on an object that captures self creates exactly this cycle, and [weak self] in the capture list is the fix.
Optionals
Optionals, and no implicit null
Int? is std::optional<int> with one crucial difference: there is no reference type in Swift that can be nil without a ? in its type.
#include <iostream> #include <optional> #include <string> std::optional<int> parsePort(const std::string& text) { try { return std::stoi(text); } catch (const std::exception&) { return std::nullopt; } } int main() { auto port = parsePort("8080"); // Nothing forces the check: *port on an empty optional is // undefined behavior, not an exception. if (port.has_value()) { std::cout << *port << std::endl; } std::cout << parsePort("xyz").value_or(80) << std::endl; return 0; }
func parsePort(_ text: String) -> Int? { Int(text) } // The unwrap is part of the binding, so there is no way to reach the // value without having handled nil. if let port = parsePort("8080") { print(port) } print(parsePort("xyz") ?? 80) // guard inverts it: handle the failure and leave, then carry on with // the unwrapped value in the ENCLOSING scope. func describe(_ text: String) -> String { guard let port = parsePort(text) else { return "not a port" } return "port \(port)" } print(describe("443"))
if let binds the unwrapped value only inside the branch, so there is no operator* to call on an empty optional — the undefined behavior C++ permits is not expressible. ?? is value_or, and ?. chains through nil. The construct worth stealing conceptually is guard let, which handles the failure case and exits, leaving the unwrapped value bound in the enclosing scope — so a function reads as a list of preconditions followed by the happy path, rather than as ever-deepening nesting. Force-unwrapping with ! exists and traps rather than being undefined.
Structs, Classes & Extensions
Extensions add methods to types you do not own
This is the feature a C++ programmer will miss most once they have used it, and C++ has no approximation at all.
#include <iostream> #include <string> // You cannot add a member function to std::string. A free function is // the answer, and it does not participate in method-call syntax. bool isShouting(const std::string& text) { if (text.empty()) { return false; } for (char character : text) { if (std::islower(static_cast<unsigned char>(character))) { return false; } } return true; } int main() { std::string message = "HELLO"; std::cout << std::boolalpha << isShouting(message) << std::endl; return 0; }
// An extension adds methods to an existing type — including one from // the standard library, in a file that type has never heard of. extension String { var isShouting: Bool { !isEmpty && !contains { $0.isLowercase } } } let message = "HELLO" print(message.isShouting) // Extensions also work on generic types, and can be conditional. extension Array where Element: Numeric { var total: Element { reduce(0, +) } } print([1, 2, 3].total)
An extension can add methods, computed properties, initializers and protocol conformances to any type, including one from another module — so the standard library's own types gain your vocabulary without wrappers or adapters. The conditional form (where Element: Numeric) is the part with real power: it adds methods only to the instantiations that qualify, which is what C++ needs partial specialization or a constrained free function template for. What extensions cannot do is add stored properties, since that would change the type's layout.
Computed properties and observers
Like C#, Swift lets a stored property become a computed one without changing any caller. Unlike C#, it also lets a stored property gain behavior.
#include <iostream> class Rectangle { public: Rectangle(double width, double height) : width_(width), height_(height) {} double width() const { return width_; } void setWidth(double value) { std::cout << "width changing to " << value << std::endl; // by hand width_ = value; } double area() const { return width_ * height_; } private: double width_; double height_; }; int main() { Rectangle card(3.0, 4.0); card.setWidth(6.0); std::cout << card.width() << " " << card.area() << std::endl; return 0; }
struct Rectangle { var width: Double { didSet { print("width changed from \(oldValue)") } // an OBSERVER } var height: Double var area: Double { width * height } // computed, reads as a field } var card = Rectangle(width: 3.0, height: 4.0) card.width = 6.0 print(card.width, card.area)
willSet and didSet attach code to an ordinary stored property, so no accessor pair is needed to observe a change — which is why Swift code rarely has hand-written setters at all. A computed property (area) is a method that reads as a field and can be read-write with an explicit get/set. The knock-on effect for a C++ programmer is that the convention of writing getters and setters up front, purely to preserve the option of changing the implementation later, is simply unnecessary here.
Protocols vs Abstract Classes
Protocols, and retroactive conformance
A protocol is an interface, and protocol extensions plus retroactive conformance make it do things an abstract base class cannot.
#include <iostream> #include <memory> #include <vector> class Shape { public: virtual ~Shape() = default; virtual double area() const = 0; // Shared behavior and shared STATE arrive together. void describe() const { std::cout << "area " << area() << std::endl; } }; class Square : public Shape { public: explicit Square(double side) : side_(side) {} double area() const override { return side_ * side_; } private: double side_; }; int main() { std::unique_ptr<Shape> shape = std::make_unique<Square>(3.0); shape->describe(); return 0; }
protocol Shape { var area: Double { get } } // A protocol EXTENSION supplies shared behavior with no shared state // and no base class. extension Shape { func describe() { print("area \(area)") } } struct Square: Shape { let side: Double var area: Double { side * side } } let shape: any Shape = Square(side: 3.0) shape.describe() // Retroactive conformance: make a type you did not write satisfy a // protocol you did. extension Int: Shape { var area: Double { Double(self) } } print(42.area)
The half that survives from inheritance is shared behavior, via protocol extensions with default implementations; the half that does not is shared state, since a protocol declares no storage. What is genuinely beyond C++ is retroactive conformance — extension Int: Shape makes an existing type satisfy a protocol declared elsewhere, which is Rust's orphan-rule-bounded trait impls and has no C++ counterpart. Note any Shape: Swift now requires the any keyword to mark an existential (a runtime-dispatched box), so the cost of dynamic dispatch is visible at the use site rather than implied.
some vs any — static and dynamic dispatch, spelled
C++ spells the static/dynamic choice with two completely different constructs. Swift spells it with two keywords in the same position, which makes the cost legible.
#include <iostream> #include <memory> class Shape { public: virtual ~Shape() = default; virtual double area() const = 0; }; class Square : public Shape { public: explicit Square(double side) : side_(side) {} double area() const override { return side_ * side_; } private: double side_; }; // Template: static dispatch, monomorphized, inlined. template <typename ShapeType> double areaOf(const ShapeType& shape) { return shape.area(); } // Base pointer: dynamic dispatch, through a vtable. double areaOfDynamic(const Shape& shape) { return shape.area(); } int main() { Square square(3.0); std::cout << areaOf(square) << " " << areaOfDynamic(square) << std::endl; return 0; }
protocol Shape { var area: Double { get } } struct Square: Shape { let side: Double var area: Double { side * side } } // `some` — an OPAQUE type: one concrete type, resolved at compile // time, dispatched statically. This is the template. func areaOf(_ shape: some Shape) -> Double { shape.area } // `any` — an EXISTENTIAL: a box that can hold any conforming type, // dispatched dynamically. This is the base-class pointer. func areaOfDynamic(_ shape: any Shape) -> Double { shape.area } let square = Square(side: 3.0) print(areaOf(square), areaOfDynamic(square))
some Shape is an opaque type: one concrete type known at compile time, specialized and statically dispatched — the template. any Shape is an existential: a box that can hold any conforming value, dispatched through a witness table and possibly heap-allocated if the value is large — the base-class pointer. Swift made any mandatory in version 6 precisely so this cost stops being invisible, which is a deliberate correction of an earlier design where a bare protocol name meant the expensive one. The rule of thumb is some unless you genuinely need a heterogeneous collection.
Templates vs Generics
Generics are constrained and checked once
Swift generics are nominal and checked up front, like Rust's — the opposite of C++'s structural, checked-at-instantiation templates.
#include <iostream> #include <vector> // Checked at instantiation, so an error points into the body rather // than at the signature. template <typename Element> Element largest(const std::vector<Element>& values) { Element best = values[0]; for (const Element& value : values) { if (value > best) { best = value; } } return best; } int main() { std::cout << largest(std::vector<int>{3, 9, 2}) << std::endl; std::cout << largest(std::vector<double>{1.5, 0.5}) << std::endl; return 0; }
// The constraint is in the signature, so the body is checked ONCE, // against Comparable, before anyone instantiates it. func largest<Element: Comparable>(_ values: [Element]) -> Element { var best = values[0] for value in values where value > best { best = value } return best } print(largest([3, 9, 2])) print(largest([1.5, 0.5]))
The body may only use what Comparable guarantees, so a mistake is reported at the definition rather than unrolling into the standard library at the call. Implementation is a hybrid: the compiler emits one generic version using witness tables, and specializes hot instantiations like a template when it can see across the module boundary — which is why @inlinable exists. What C++ keeps that Swift gives up is the metaprogramming: no specialization you control, no non-type parameters until recently, and nothing resembling a compile-time computation language. Note for … where, which folds the filter into the loop.
Associated types
An associated type is the protocol's way of saying "the conforming type decides what this is", and it is how Collection, Sequence and Iterator are built.
#include <iostream> #include <vector> // A C++ concept can require a member TYPE, and the type is reached // through the class itself. template <typename Container> concept HasValueType = requires { typename Container::value_type; }; template <HasValueType Container> typename Container::value_type firstOf(const Container& container) { return *container.begin(); } int main() { std::vector<int> readings{7, 8}; std::cout << firstOf(readings) << std::endl; return 0; }
// An associated type is a placeholder the conforming type fills in. protocol Container { associatedtype Item var first: Item { get } } struct IntBox: Container { let values: [Int] var first: Int { values[0] } // Item is INFERRED as Int } func firstOf<C: Container>(_ container: C) -> C.Item { container.first } print(firstOf(IntBox(values: [7, 8])))
The conforming type supplies the concrete type, usually inferred from the implementation, and callers reach it as C.Item. This is close to a C++ concept requiring a nested typedef, with the difference that the relationship is declared and checked. The wrinkle worth knowing is that a protocol with associated types could not be used as an existential at all until Swift 5.7 — the notorious "protocol can only be used as a generic constraint" error — and any Container now works through primary associated types and type erasure.
Enums & Pattern Matching
std::variant vs enums with associated values
A Swift enum case can carry values, which makes it a tagged union with first-class syntax — and it is exhaustiveness-checked.
#include <iostream> #include <string> #include <variant> using Message = std::variant<int, std::string>; int main() { Message message = std::string("hello"); std::visit([](const auto& value) { using Held = std::decay_t<decltype(value)>; if constexpr (std::is_same_v<Held, int>) { std::cout << "number " << value << std::endl; } else { std::cout << "text " << value << std::endl; } }, message); return 0; }
enum Message { case number(Int) case text(String) } let message = Message.text("hello") switch message { case .number(let value): print("number", value) case .text(let value): print("text", value) } // Exhaustive: removing an arm is a compile error, not a fallthrough.
Everything std::variant does, this does with less ceremony: no std::visit, no if constexpr ladder, no std::get that throws, no valueless-by-exception state. The cases are named, so an enum with two Double payloads meaning different things is expressible where std::variant<double, double> is useless. And the switch must be exhaustive, so adding a case turns every incomplete match into a compile error — the check -Wswitch abandons the moment a default: appears. Enums can also have methods, computed properties and protocol conformances.
Pattern matching beyond the switch
Swift's patterns cover tuples, ranges, types, enum cases and optionals, and they appear in more places than switch.
#include <iostream> #include <tuple> int main() { std::tuple<int, int> position{3, 4}; // Structured bindings destructure but cannot match, so the // conditions become a separate if/else ladder. auto [row, column] = position; if (row == 0 && column == 0) { std::cout << "origin" << std::endl; } else if (row == column) { std::cout << "diagonal" << std::endl; } else if (row > 0 && column > 0) { std::cout << "first quadrant" << std::endl; } else { std::cout << "elsewhere" << std::endl; } return 0; }
let position = (3, 4) switch position { case (0, 0): print("origin") case let (row, column) where row == column: print("diagonal") case let (row, column) where row > 0 && column > 0: print("first quadrant") default: print("elsewhere") } // Ranges are patterns too, and `if case` matches a single shape. let score = 87 switch score { case 90...: print("A") case 80..<90: print("B") default: print("C") }
A case can destructure and bind (case let (row, column)), filter with where, and match a range (80..<90) — which C++ needs an if/else ladder for even after structured bindings. The same patterns work in if case, guard case and for case, so the syntax pays for itself across the language rather than only inside a switch. Two details for a C++ reader: switch never falls through (there is a fallthrough keyword for the rare case), and it must be exhaustive, so default is required whenever the compiler cannot prove coverage.
Error Handling
throws is checked, and typed
Swift's errors are exceptions in shape and return values in mechanism, and the compiler checks every call.
#include <iostream> #include <stdexcept> #include <string> // Any function may throw unless marked noexcept, and the signature // does not say what. Propagation through callers is invisible. int parsePort(const std::string& text) { int value = std::stoi(text); if (value < 0) { throw std::out_of_range("negative"); } return value; } int main() { try { std::cout << parsePort("8080") << std::endl; std::cout << parsePort("-1") << std::endl; } catch (const std::exception& problem) { std::cout << "failed: " << problem.what() << std::endl; } return 0; }
enum PortError: Error { case notANumber case negative } // Swift 6 typed throws: the signature names EXACTLY what can go wrong. func parsePort(_ text: String) throws(PortError) -> Int { guard let value = Int(text) else { throw PortError.notANumber } guard value >= 0 else { throw PortError.negative } return value } do { print(try parsePort("8080")) print(try parsePort("-1")) } catch { print("failed: \(error)") // `error` is bound implicitly }
A function that can fail must be marked throws, and every call to it must be marked try — so unlike C++, where any call may unwind invisibly, every possible early exit is visible in the source. Swift 6's typed throws (throws(PortError)) go further and let the compiler check a catch for exhaustiveness, which is closer to Rust's Result than to an exception. Under the hood there is no stack unwinding: errors are returned in a register, so the cost is a branch. try? converts a failure to nil and try! traps, which are the two escape hatches.
defer, alongside deinit
Swift has both mechanisms: deinit on a class is a destructor, and defer covers the cases where a type would be overkill.
#include <iostream> #include <string> class ScopeGuard { public: explicit ScopeGuard(std::string name) : name_(std::move(name)) { std::cout << "begin " << name_ << std::endl; } ~ScopeGuard() { std::cout << "end " << name_ << std::endl; } private: std::string name_; }; void work() { ScopeGuard guard("payment"); // cleanup attached to the TYPE std::cout << "working" << std::endl; } int main() { work(); return 0; }
func work() { print("begin payment") // defer runs at scope exit — on return, on throw, on break. defer { print("end payment") } print("working") } work()
Unlike Go's, Swift's defer is scoped to the enclosing block rather than the function, and deferred blocks run in reverse order on any exit — return, thrown error, or leaving a loop body. Note what Swift keeps that Go and Zig do not: a class has deinit, which is a genuine destructor running deterministically under ARC, so a type can guarantee its own cleanup for its users. So the choice is the C++ one — put it in the type when several callers need it, use defer for one-off local cleanup — rather than a workaround for a missing feature.
Strings
Strings are collections of grapheme clusters
Swift went further on Unicode than any other mainstream language, and the consequence is an API that will feel obstructive until you see why.
#include <iostream> #include <string> int main() { std::string greeting = "naïve"; // Bytes, with no encoding attached. Indexing gives one byte, // which may be half a character. std::cout << "bytes: " << greeting.size() << std::endl; std::size_t characters = 0; for (unsigned char byte : greeting) { if ((byte & 0xC0) != 0x80) { characters += 1; } } std::cout << "characters: " << characters << std::endl; return 0; }
let greeting = "naïve" // count is GRAPHEME CLUSTERS — what a reader calls a character — // not bytes and not even code points. print("bytes:", greeting.utf8.count) print("characters:", greeting.count) // Which is why there is no integer subscript: finding the nth // character requires walking, so the API makes that cost visible. let second = greeting[greeting.index(greeting.startIndex, offsetBy: 1)] print("second:", second)
A Character is an extended grapheme cluster — what a human calls one character even when it is several code points, like an emoji with a skin-tone modifier — so count is the answer a user would give and comparison handles canonical equivalence correctly. The price is that greeting[2] does not compile: locating the nth cluster requires walking from the start, and Swift refuses to hide an O(n) operation behind subscript syntax. That honesty is the single most-complained-about thing in the language, and it is also why Swift string handling is correct where std::string's is merely fast.
Ownership: ~Copyable & borrowing
~Copyable is a move-only type, checked
Swift 5.9 added non-copyable types, which is the same idea as a move-only C++ type with the use-after-move hole closed.
#include <iostream> #include <memory> #include <utility> // A move-only type: copy deleted, move defaulted. Using the // moved-from object is legal and yields an unspecified value. struct FileHandle { int descriptor = 0; FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; FileHandle(FileHandle&& other) noexcept : descriptor(other.descriptor) { other.descriptor = -1; // you must clear it BY HAND } explicit FileHandle(int value) : descriptor(value) {} }; int main() { FileHandle first(3); FileHandle second = std::move(first); // Reading first.descriptor here is legal and gives -1 only // because the move constructor was written to do that. std::cout << second.descriptor << " " << first.descriptor << std::endl; return 0; }
struct FileHandle: ~Copyable { let descriptor: Int } func consume(_ handle: consuming FileHandle) { print("using", handle.descriptor) } // A noncopyable value has to live in a local scope — a global one // cannot be consumed at all. func run() { let first = FileHandle(descriptor: 3) consume(first) // print(first.descriptor) // COMPILE ERROR: 'first' used after consume } run() print("done")
A ~Copyable type has no copy at all, and the compiler tracks its single owner — so using a value after it has been consumed is a compile error, not a legal read of an unspecified state. That is the guarantee C++ cannot make: a moved-from object is "valid but unspecified", and you must remember to write a move constructor that leaves it in a sensible one. The parameter modifiers spell the rest: consuming takes ownership, borrowing is const&, and inout is a mutable reference. If this sounds like Rust, it is — Swift adopted the useful half without the lifetime annotations. A ~Copyable struct may also have a deinit, which an ordinary struct cannot; it is left out of the example above deliberately, because the exact point at which a consumed value is destroyed changed between Swift 6.2 and 6.3 — so a row whose output depended on destruction timing would report one thing under one toolchain and another under the next.
Structured Concurrency & Actors
Structured concurrency
Swift's concurrency is structured: a child task's lifetime is bounded by its parent's scope, which is a guarantee C++ has no equivalent of.
#include <future> #include <iostream> int compute(int value) { return value * value; } int main() { // std::async returns a future; nothing structures the lifetime, // and forgetting to wait on the future BLOCKS in its destructor. auto first = std::async(std::launch::async, compute, 3); auto second = std::async(std::launch::async, compute, 4); std::cout << first.get() + second.get() << std::endl; return 0; }
func compute(_ value: Int) async -> Int { value * value } // A task group: the child tasks CANNOT outlive the group, so there is // no way to leak one or forget to await it. func total() async -> Int { await withTaskGroup(of: Int.self) { group in group.addTask { await compute(3) } group.addTask { await compute(4) } var sum = 0 for await result in group { sum += result } return sum } } print(await total())
A task group cannot return until its children finish, so a task cannot be leaked, orphaned, or forgotten — and cancellation propagates down the tree automatically. Compare std::async, where the future's destructor blocking is a notorious surprise and nothing relates one task to another. await marks every suspension point, so you can see exactly where other work may interleave. The reason this matters beyond ergonomics is the next row: knowing the task tree is what lets the compiler check for data races.
Actors, and data races rejected at compile time
Swift 6 turned data-race safety into a compile-time property, which puts it in the same category as Rust rather than as C++ or Go.
#include <iostream> #include <mutex> #include <thread> #include <vector> int main() { int total = 0; std::mutex guard; // the mutex and the data are separate, // and nothing checks you took the lock std::vector<std::thread> workers; for (int number = 0; number < 4; number += 1) { workers.emplace_back([&total, &guard]() { std::lock_guard<std::mutex> held(guard); total += 10; }); } for (std::thread& worker : workers) { worker.join(); } std::cout << total << std::endl; return 0; }
// An actor owns its state: every access from outside is serialized, // and reaching it without awaiting does not compile. actor Total { private var value = 0 func add(_ amount: Int) { value += amount } func get() -> Int { value } } let total = Total() await withTaskGroup(of: Void.self) { group in for _ in 0..<4 { group.addTask { await total.add(10) } } } print(await total.get())
An actor is a reference type whose mutable state is isolated: calls from outside are serialized and must be awaited, so the association between lock and data is enforced by the type rather than remembered. The wider mechanism is Sendable — a marker for types safe to cross an isolation boundary, checked by the compiler — which is Swift's equivalent of Send/Sync. The honest caveat is that this is much newer than Rust's: strict concurrency checking became the default only in Swift 6, and migrating an existing codebase to it is real work that the community is still in the middle of.
C++ Interoperability
Swift calls C++ directly
This is the reason a C++ programmer should care about Swift at all, and it is only two years old.
// The C++ side needs NOTHING special — no extern "C", no C shim, // no manual bindings. A header with ordinary C++ in it: // // // Geometry.h // #pragma once // #include <string> // struct Point { // double x, y; // double length() const { return std::sqrt(x*x + y*y); } // }; // std::vector<Point> makePoints(); #include <cmath> #include <iostream> struct Point { double x; double y; double length() const { return std::sqrt(x * x + y * y); } }; int main() { Point corner{3.0, 4.0}; std::cout << corner.length() << std::endl; return 0; }
// The Swift side imports the header as a MODULE and uses the C++ // types as if they were Swift ones — methods, templates, operators // and all. This is illustrative: the page's runner compiles a single // Swift file with no C++ header alongside it. // // import Geometry // the C++ module // // let corner = Point(x: 3.0, y: 4.0) // print(corner.length()) // a C++ member function, called directly // // for point in makePoints() { ... } // std::vector conforms to Sequence // // Enabled with -cxx-interoperability-mode=default, or in Package.swift // via .interoperabilityMode(.Cxx). Shipped in Swift 5.9. import Foundation struct Point { let x: Double let y: Double func length() -> Double { (x * x + y * y).squareRoot() } } print(Point(x: 3.0, y: 4.0).length())
Swift 5.9 shipped bidirectional C++ interoperability: Swift imports C++ classes, methods, templates and operators from a header with no shim and no generated bindings, and C++ can call Swift back through a generated header. That is a different position from every other language on this anchor — Rust, Go and C# all meet C++ across a C-shaped boundary. The limits are real: std::vector and std::string map well, ownership must still be reasoned about by hand across the line, and virtual inheritance and exceptions are partly supported. But "call your existing C++ from a new component" is now a genuine option rather than a project.
What Swift Trades Away
What you keep and what you trade
Worth ending on the strategic question rather than the technical one, because for this target that is what actually decides it.
// What C++ keeps that Swift does not offer: // // Templates as a metaprogramming language → constrained generics // Deterministic destruction WITHOUT ARC → ARC, with refcount traffic // Zero-overhead abstraction, guaranteed → mostly, with retain/release // Multiple compilers and an ISO standard → one vendor, one compiler // First-class support on every platform → Apple first, Linux second // Predictable, uniform string indexing → O(n) by grapheme cluster // const-correctness on references → value semantics instead #include <iostream> int main() { std::cout << "portable, permanent, and yours to manage" << std::endl; return 0; }
// What you get for it: // // Value semantics with copy-on-write, so aliasing bugs disappear // Optionals, so null-dereference is a type error // ~Copyable and consuming, so use-after-move does not compile // Actors and Sendable, so data races are checked at compile time // Exhaustive switch over enums with associated values // Protocol extensions and retroactive conformance // Deterministic cleanup via deinit — no collector, no pauses // And direct C++ interop, so adopting it is incremental // // The honest summary: Swift is the safest language that can call your // C++ classes without a shim. Whether that matters depends almost // entirely on whether you ship on Apple platforms. print("safer, newer, and narrower")
Technically Swift is a strong package: value semantics remove aliasing bugs, optionals remove null dereferences, ~Copyable removes use-after-move, and Swift 6 removes data races — a safety story close to Rust's with a much gentler learning curve, and deterministic cleanup that neither Go nor C# offers. The constraint is reach. One vendor sets the roadmap, Linux and Windows support trails, and the ecosystem outside Apple's frameworks is thin compared with what a C++ project can draw on. If you ship on Apple platforms, the C++ interop makes incremental adoption genuinely practical and Swift is the obvious choice. If you do not, Rust answers most of the same problems with broader reach.