1. 17
  1.  

  2. 5

    In this case, passing a pointer into a function is still passing by value in the strictest sense, but it’s actually the pointer’s value itself that is being copied, not the thing that the pointer refers to

    Is this not how every language works when handling pointers?

    1. 6

      I think so, but I believe the main point of the article is how there are certain types, like slices, maps, and channels, that feel as if you’re passing them by value, even though they behave like references.

      This sometimes trips people up (like me), for example: https://eli.thegreenplace.net/2018/beware-of-copying-mutexes-in-go/

      1. 8

        I learned recently that go vet will give that warning on copying any struct which implements Lock() and Unlock() methods.

        E.g.,

        package main
        type t struct{}; func (*t) Lock() {};func (*t) Unlock() {}
        func main() { a := t{}; _ = a }
        

        will trigger the vet warning.

      2. 2

        C++ references are distinct! For example in Python (and I imagine in Go as well) you can’t pass a reference to an integer. You can’t do

        x = 3
        f(x)
        # x now equals 4
        

        (in CPython you can by doing some stack trace trickery)

        This is kind of linked back to a fundamental C++-ism of built-ins being “the same as” other data types. Whereas Python/Java/Go/lots of other stuff have this distinction between builtins and aggregate types.

        Rust, being the true successor to C++ in so many ways, carries over references nicely tho…

        fn f(x: &mut i32){
            *x += 1;
        }
        
        fn main() {
            let mut x:i32=4;
            println!("x={}", x);
            f(&mut x);
            println!("x={}", x);
            println!("Done");
        }
        

        And beyond “changing the contents of an integer”, ultimately being able to change the variable itself (even replacing it with an entirely different object) is only really an option in systems languages.

        1. 1

          The only exceptions I can think of are:

          • perl - lists are copied
          • tcl - lists are strings
          • C - structurs are actually copied without explicit pointers
          • languages with explicit value types like C#
          1. 3

            Thanks for sharing that, it’s an excellent summary and it reminds me of one of the things that I really struggled with in understanding pointers in C back when I first learned the language in the distant past: All variables are pointers but a lot pretend not to be. If I write int x, what I mean is that x is a reference to an int somewhere in memory. If it’s on the stack, the pointer is represented as a fixed offset from the stack pointer. If it’s in a struct, then it’s encoded as a fixed offset from the start of the object. But, at the abstract machine level, it’s still a pointer. If I write int *x then I now have a name that defines a pointer to a pointer to an integer.

            This is a lot more jarring in Java, where there’s no syntactic disambiguation between single and double indirection. int x in Java is a pointer to an integer, Object x is a pointer to a pointer to an object. In both cases, the compiler will probably statically fold the first indirection into an immediate offset from some other pointer (object or stack pointer), but it’s still there in the abstract machine. This gets really confusing because if int x is a parameter and I mutate x then the behaviour is different to the equivalent behaviour if I passed Object x and mutated x.

            This is much simpler in Smalltalk. x in Smalltalk is always a pointer to some object. If it’s a pointer to a small integer object then, as an optimisation, the value is embedded in the pointer. This is not observable at the abstract machine level, because small integer objects are immutable and so there’s no way that you can tell the difference between receiving a pointer to the SmallInt instance that the parent had or a distinct one that has the same value.

            This is the approach that we take in Verona. Types like Builtin.U32 are still, at the abstract machine level, accessed by pointer but they only way of creating a U32 actually creates a U32 & imm (i.e. a pointer to a globally immutable 32-bit integer). As an optimisation, the representation will elide a layer of indirection and if you have a U32 field then it will be stored as 32 bits of data inline, rather than as a pointer.

          2. 1

            I like this definition/explanation (from the Julia docs, buts lots of languages have the same argument passing behaviour):

            Julia function arguments follow a convention sometimes called “pass-by-sharing”, which means that values are not copied when they are passed to functions. Function arguments themselves act as new variable bindings (new locations that can refer to values), but the values they refer to are identical to the passed values. Modifications to mutable values (such as Arrays) made within a function will be visible to the caller. This is the same behavior found in Scheme, most Lisps, Python, Ruby and Perl, among other dynamic languages.

            I like this because it specifies a behaviour that is simple and makes sense (in Julia you only need to know about variable bindings, values and that values can be mutable or immutable) while also being more honest about what the compiler is doing: copying isn’t mentioned above because it is a language implementation detail - the compiler can copy values or it might not (e.g. when inlining, or when only some of the fields of a struct are being used).