1. 3
  1.  

    1. 2

      I was confused about the concept of return values (~21:00) Luca mentioned in the video.

      The local variable of the struct should be on the stack and could be clobbered, no? Or is the compiler doing some magic there?

      1. 5

        Returned values are returned to the caller. Older C standards/compilers would only allow you to return scalar values/pointers (basically they had to fit in a register being a good rule of thumb), but “modern” C you can return anything and the compiler will insert memory copying operations as needed.

        Think of it like this:

        int
        foo(void)
        {
            int r = 1 + 2;
            return r;
        }
        

        r is allocated on the stack (theoretically) but it has no problem being returned. Used to be you couldn’t do that with structs, but now you can.

        1. 2

          compiler will insert memory copying operations as needed.

          Perfect. Thank you for the explanation!

          1. 5

            C assignments and function calls use value semantics (copying), EXCEPT in the case of arrays, which is probably where the confusion comes from.

            If you assign structs, you get copies. If you return structs, you get copies. If you pass structs as params, you get copies. (I would not think of it as having anything to do with the “compiler”; this is just the definition of the language.)

            But not arrays: Arrays decay to pointers. This is a big wart is a special case in the language that was probably done for “efficiency”, but ends up just confusing things.

            (Another problem is that arrays in C are fixed-size, and the size is part of their type. So to have dynamically sized arrays, which is basically always what you need in practice, you usually end up with pointer/length pairs.)

            1. 4

              If you assign structs, you get copies. If you return structs, you get copies. If you pass structs as params, you get copies.

              I know this is a little pedantic, but the return case is not observable and thus moot. If you pass a struct as a param, the caller had access to the memory before the call and the callee had access afterwards, so a compiler must ensure that modifications in the child function do not propagate to the parent to be correct. A copy is the generic way to do this, although the copy can be optimized out if modifications do not occur. If you return a struct, the parent function did not have access to the child’s return value until it called the child function, and the child function cannot access it after returning, so a copy is not required to ensure correctness. The optimization is always present.

              In a previous project I wrote code that returned a struct that contained two void pointers on x64. The compiler was smart enough to return it in xmm0, a 128-bit register.

              1. 2

                It can be optimized but I don’t think that makes the point moot. I would think about it from the perspective of language semantics first (value semantics and copying), and then second about what optimizations can be made.

                Also I think there are many cases where the compiler can’t do anything but copy, like:

                struct Point f() {
                  struct Point p1 = { 3, 4 };
                  if (somecondition()) {
                     struct Point p2 = { 5, 6 };
                     return p2;
                  }
                  return p1;
                }
                

                I think that is basically the reason for the RVO rule in C++: you have to help the compiler do the optimization by putting it first on the stack.

                1. 2

                  I don’t think that example requires a copy, and I’m really curious if such an example could ever be provided.

                  The way I read this code, the caller to f() needed to reserve space in its stack for the return value, and f() needs to know that location to populate it. At the time of the call, that memory contains undefined contents. On return from f(), it contains defined contents. Ergo, f() is free to manipulate that location - in the stack of its parent - without having to stage anything in f()’s stack frame first. It is free to execute any logic it wants in deciding what to put in that memory, but it can manipulate the parent’s stack directly, without having to stage to its own stack and copy on return.

                  It is true that code can be written which causes that memory to be written to many times, but that does not mean it must be written to many times and then copied.