Hacker News new | ask | show | jobs
by Thorrez 1749 days ago
>Honestly, this is a strange and peculiar situation, although Go programmers have acclimatized to it. To programmers from other languages, such as C or C++, the concept of pointers to dynamically extensible arrays seems like a perfectly decent idea that surely should exist and work in Go. Well, it exists, and it "works" in the sense that it yields results and doesn't crash your program, but it doesn't "work" in the sense of doing what you'd actually want.

I think it works in the same way as a pointer to std::span in C++. (Or pointer to std::string_view with the exception that std::string_view doesn't allow modification of the elements.)

I guess the difference is that std::span doesn't let you append to the backing array through the std::span directly. So with C++ you have to write more code which makes it clearer what's happening.

1 comments

C++ has std::vector, which is one level of abstraction above a slice; you push to a vector, and maybe the backing slice changes, but it's still the same vector.

Go is unusual in not having a equivalent of vector.

Yes, std::vector and slice are different. My point is that a slice is similar to std::span.

With both slice and std::span, = does a shallow copy of just 2 or 3 pointers.

With std::vector = does a deep copy of every element, you have 2 distinct backing arrays.

Go forces you to use copy() and append() and rely on the garbage collector to make a slice fill the roll of std::vector. IMO it leads to some confusing code.

Not really, if reallocation takes place.

So if you got a pointer to a vector element, it now points to garbage.

I guess the difference is this:

C++:

  std::vector<int> v {1, 2, 3};
  void foo(std::vector<int> *ref) {
    ref.push_back(4); 
  }
  foo(&v);
  //v[3] == 4 is true here
Go:

  v := []int{1, 2, 3}
  func foo(ref *[]int) {
    append(ref, 4)
  }
  foo(&v)
  //v[3] == 4 may or may not be true here.
Pointers to elements in the vector do indeed have the same problems both in Go and C++ (except for memory safety).
Ah ok, although for the audience not versed in C++ the correct code is,

  void foo(std::vector<int> *ref) {
    ref->push_back(4); 
  }
  foo(&v);
or

  void foo(std::vector<int> &ref) {
    ref.push_back(4); 
  }
  foo(v);
Oops, right, bit rusty on C++...
append(ref, 4) // error: first argument to append must be slice

append(*ref, 4) // error: append(*ref, 4) evaluated but not used

this is the correct version and it also removes the incertitude:

*ref = append(*ref, 4)

https://play.golang.org/p/-xDqaxvqWhm

Yes, I made a significant error in forgetting what slice variables actually represent. In my example, even fixing the compilation errors (with `_ = append(*ref, 4)` ), v[3] would always be an array index out of bounds error, since v itself always points to just the first 3 elements of the array, regardless of resizing. This is another significant difference between slices and C++ vectors.

A more interesting example showing that resizing can be observed (getting rid of the pointer to the slice, since it's not useful anyway):

https://play.golang.org/p/UJ-t63bKyJJ

Too late to edit, but I made a significant mistake in the Go code: v[3] would always be a runtime error unless we explicitly modify *ref inside the function (ref = append(ref, 4), in which case v[3] == 4 would always be true).

This happens regardless of resizing, since append() at best modifies the array that *ref/v points to, but it does not modify *ref/v itself; and slices in Go have a pointer to an underlying storage AND a start and end index into that storage (multiple slices can point to different parts of the same storage).

Created here an example that shows how this interacts with resizing:

https://play.golang.org/p/UJ-t63bKyJJ

> //v[3] == 4 is true here

Note that C++ std::vector has an operator overload so this is actually just calling the method named operator[] on the object v and that method returns you a reference to the object in the backing array if in fact it is a suitable size (no checks are made). In particular if foo doesn't for any reason extend the vector then our program now has Undefined Behaviour.

This is not merely the C syntactic sugar array subscript operation v[3] == *(v+3) as the vector is not necessarily just a backing array pointer and some magic.

In contrast Go is really offering array syntax for this slice, that's the built-in array subscript operation and it cares whether v[3] exists when you try to compare it to 4.

I'm not sure I understand how this is relevant. In C++, arr[non_existent_index] is UB both if arr is an std::vector and if it is a C-style array. In Go it's a runtime error instead.

Sure, std::vector::operator[]() is not just syntax sugar for *(v+i), but I don't think any of this is relevant for the discussion at hand, unless I'm missing something.