Immutability - Swift vs C++

Immutability awareness is on the rise. Especially when the codebase grows in size and complexity, the benefits of immutability for stability, readability and maintenance reasons is imminent.

If you’re dealing a function that receives immutable data as arguments, you don’t have to bother checking for any side effects. Or worrying about accidentally mutating the received data.

void process(const Page & page) {
    // ...
}

Similar benefits can be observed from the other side. If you don’t expect data to be mutated, you might prefer marking the entire method as immutable.

struct Page {
    bool print() const {
        // ...
        return true;
    }
};

void process(const Page &page) {
    bool status = page.print();
    // ...
}

But things get a bit complicated when you’re not dealing with the first hand data, rather a reference to the data. Since the reference is just a data holding the data. So depending on the intended design, either the data or the reference could be mutable, or both, or none.

As we can see, we are dealing with 2 things here each of which can then be in 2 states. So overall we are dealing with 4 cases:

Everything is mutable

Let’s consider a simple C++ data structure

struct Foo {
    int x;
};

Marking everything as mutable is very easy. Do nothing special.

void update_all(Foo *f) {
    f->x = 10;
    f = nullptr;
}

With Swift we need to make the decision about mutability while designing the data structures. Since the data within a struct is always immutable and within a class may or may not be mutable. The mutability of reference depends on whether is a let or a var.

So, in case of a struct we would have to explicitly set the data and the reference to be var

struct A {
    var x = 0
}

func updateAllStruct() {
    var f = A()
    f.x = 10
    f = A()
}

Note that since A is struct type, so every mutating operation internally creates a new copy of the entire data structure.

The class behaves just like the C++ counterpart as long as keep both data and reference as var

class B {
    var x = 0
}

func updateAllClass() {
    var f = B()
    f.x = 10
    f = B()
}

Reference is mutable

If we only need a mutable reference but the data should remain immutable. With C++ we simply need to mark the pointed data as const. If we then accidentally try to mutate the data, the compiler would throw an error.

void update_ref(Foo const * f) {
    f->x = 10; // error: Cannot assign to variable 'f' with const-qualified type 'const Foo *'
    f = nullptr; // good
}

In Swift, if we wish the data to be immutable, we have to mark it as let while keeping the reference as var

struct C {
    let x = 0
}

class D {
    let x = 0
}

And then, if we accidentally try to mutate the via an mutable reference, the compiler would throw an error

func updateRefStruct() {
    var f = C()
    f.x = 10 // error: Cannot assign to property: 'x' is a 'let' constant
    f = C() // good
}

func updateRefClass() {
    var f = D()
    f.x = 10 // error: Cannot assign to property: 'x' is a 'let' constant
    f = D() // good
}

Data is mutable

With C++, marking the data as mutable while the reference remains immutable also seems quite straightforward. Simply mark the reference as const

void update_data(Foo * const f) {
    f->x = 10; // good
    f = nullptr; // error: Cannot assign to variable 'f' with const-qualified type 'Foo *const'
}

In Swift however, having a struct where reference is immutable while data as mutable is not possible. Because, as soon as you mark the reference as let, the internal storage becomes immutable as well, even if it was marked as var

func updateDataStruct() {
    let f = A()
    f.x = 10 // error: Cannot assign to property: 'f' is a 'let' constant
    f = A() // error: Cannot assign to value: 'f' is a 'let' constant
}

But, again we can use class type to get the same behavior as C++

func updateDataClass() {
    let f = B()
    f.x = 10 // good
    f = B() // error: Cannot assign to value: 'f' is a 'let' constant
}

Everything is immutable

Marking everything as immutable with C++ is definitely possible, but requires a lot of extra typing. You need to mark both the reference and the pointed data as const

void update_none(Foo const * const f) {
    f->x = 10; // error: Cannot assign to variable 'f' with const-qualified type 'const Foo *const'
    f = nullptr; // error: Cannot assign to variable 'f' with const-qualified type 'const Foo *const'
}

With Swift, if we mark both the reference and the data as let everything becomes immutable

func updateNoneStruct() {
    let f = C()
    f.x = 10 // error: Cannot assign to property: 'x' is a 'let' constant
    f = C() // error: Cannot assign to value: 'f' is a 'let' constant
}

func updateNoneClass() {
    let f = D()
    f.x = 10 // error: Cannot assign to property: 'x' is a 'let' constant
    f = D() // error: Cannot assign to value: 'f' is a 'let' constant
}

Opinions?

I personally like C++ way of dealing with mutability. It seems simpler to understand and offers more control to the user of the API. Compared to Swift where you need to look the implementation of the library to see if the type is a class or a struct.

Also from the author’s perspective, they only have to design the class intrinsic behavior for ‘const correctness’ by marking appropriate methods as only available when accessed via as const.

What’s even more interesting is the fact that the authors can even override methods for const to give even more flexible class interface

struct Foo {
    int increment() { return ++x; }
    int increment() const { return x + 1; }
    int x = 0;
};

void f() {
    const Foo f;
    int y = f.increment();
    printf("%d %d\n", y, f.x); // 1 0
}

void g() {
    Foo f;
    int y = f.increment();
    printf("%d %d\n", y, f.x); // 1 1
}

In fact, this technique is used by the std::map operator[]. It’s called const-overloading

The thing I like about Swift though is the immutable by default behavior in Swift, which makes code safer by default, unlike C++ where you have to discipline yourself to it. And some cases where having things as immutable is even frowned upon.