1. 34
  1.  

  2. 17

    Perhaps nitpicky, but I wouldn’t suggest recommending VLAs in a document like this. The consensus seems to be that it’s probably never wise to use them. They were also made optional in C11 (meaning compilers no longer have to support them and you have to test for __STDC_NO_VLA__ to check support). It’s basically syntactic sugar for the same thing alloca() does, which has all the same problems as VLAs:

    The basic argument against them goes something like this: if the variable size argument(s) for your VLAs could end up being very large at runtime, they could overflow stack space and cause a crash. If your answer to this proposed crash (for a given VLA usage) is that your VLA size arguments are already constrained to stack-reasonable sizes, then it would be more efficient, compatible, and simple to simply declare a fixed-length stack array of the maximum possible size (since stack allocations just adjust a stack frame pointer). If you really do need the possibility of very large sizes and don’t want to possibly crash, the only sane approach is going to be to use heap allocatiors like malloc() and friends.

    1. 7

      I agree with you, and I don’t find this nitpicky at all. Use of VLAs is lately discouraged in the kernel, and there’s an active effort to remove them[1]. They’re not secure, and have slower performance, so a document about pleasant C should in my opinion discourage them, or not mention them at all, but not do the opposite.

      1. 8

        I don’t really mean to be encouraging use of specific C features here, just pointing out some things that I find pleasant to write. Perhaps a CAVEATS section is needed.

        1. 3

          Probably a good idea.

          1. 2

            Fair enough, sorry for hijacking your otherwise very nice page:)

        2. 4

          Neither malloc nor alloca are capable of the behaviour I pointed out that I found useful, unfortunately.

          1. 2

            How about:

            T (*dyn)[len][h][w] = malloc(len * h * w * sizeof(T));
            fread(dyn, h * w, len, stdin);
            

            one difference is that you now have to write (*dyn)[i][j][k] instead of arr[i][j][k]. Is there any issue that I’m not familiar with that can be caused by this approach?

            1. 2

              I thought multiplying values inside of malloc sizing was bad form due to overflow risks?

              1. 1

                It would have to be done here one way or another, since calloc only does 1D arrays, sure you can do:

                size_t dyn_len = len * h * w * sizeof(T)
                T (*dyn)[len][h][w] = malloc(dyn_len);
                ...
                

                or have a function that does it:

                void *new_3d_array(size_t len, size_t h, size_t w, size_t type_size) {
                    return malloc(len * h * w * type_size);
                }
                T (*dyn)[len][h][w] = new_3d_array(len, h, w, sizeof(T));
                

                or….

                #define SIZEOF_3D_ARRAY(X, Y, Z, TYPE) ((X) * (Y) * (Z) * sizeof((TYPE)))
                T (*dyn)[len][h][w] = malloc(SIZEOF_3D_ARRAY(len, h, w, T));
                

                If you often have to work with 3D arrays then you should definitely put an extra layer there that multiplies for you, but I didn’t do that here, because the example in the page was about convenience, and I wanted to show mallocing that can be used in exactly the same way with fread.

                Edit:

                Here’s a non performant version but with the overflow handling:

                void * new_3d_array(size_t len, size_t h, size_t w, size_t sizeof_type) {
                        size_t hw = h * w;
                        if (h != 0 &&  hw / h != w)
                                return NULL;
                
                        size_t hwlen = hw * len;
                        if (hw != 0 && hwlen / hw != len)
                                return NULL;
                
                        size_t sothwlen = hwlen * sizeof_type;
                        if (hwlen != 0 && sothwlen / hwlen != sizeof_type)
                                return NULL;
                
                        return malloc(sothwlen);
                }
                
                func(size_t len, size_t h, size_t w) {
                        T (*dyn)[len][h][w] = new_3d_array(len, h, w, sizeof(T));
                        assert(dyn != NULL);
                        fread(dyn, h * w, len, stdin);  // Nothing stops h * w from overflowing..
                }
                
          2. 3

            VLAs could be more fair if there would be a way to map them to a custom heap allocator, there’s nothing in the spec itself that dictates the allocation to be intertwined with your stack frame and obvious reasons why you would want to avoid just that. The whole principle is to get access to dynamic allocation tied to the scope of ‘auto’, what pool it is taken from is secondary - but with sizeof() being possibly dynamic, get something that isn’t malloc heap or compiler managed stack.

          3. 6

            First example under-utilizes “nice” initializer syntax, in my opinion – could be written as

            struct {
                    struct pollfd fds[2];
            } loop = {
                   .fds = {
                      { .fd = STDIN_FILENO, .events = POLLIN, },
                      { .fd = STDOUT_FILENO, .events = POLLOUT, },
                    },
            };
            

            A related designated-initializer syntax for arrays is particularly useful for arrays indexed by enums:

            void handle_foo(void);
            void handle_bar(void);
            enum { Foo, Bar, };
            void (*handlers[])(void) = {
              [Foo] = handle_foo,
              [Bar] = handle_bar,
            };
            
            1. 4

              Yeah the example is a bit contrived to demonstrate that designators can be chained together rather than having to nest more braces.

            2. 3

              One that people keep forgetting in initialisers is compound literals.

              void foo(struct bar*){ … }

              foo(&(struct bar){.baz = 0xdeadbeef});

              1. 1

                Can you really use & there? iirc C won’t let you pass &0xDEADBEEF, for example.

                1. 2

                  Can you really use & there?

                  Try it out and see…if your compiler implements C99 it should work.

                  iirc C won’t let you pass &0xDEADBEEF, for example.

                  Not with that exact syntax, but you can do e.g. int* foo = (int[]){ 12, };.

              2. 1

                For the leading-break, this is just a way to put a break for the previous case. In fact, my auto-formatter just moves it to the normal place for the previous case. It’s another way to remember to always break your cases I guess.

                1. 1

                  Yes, but it formats nicer for single-line cases and it is much easier to spot a missing break.

                  1. 1

                    What I’m saying is (for my formatter at least) is it won’t do that as a single line.

                    But I get it, it shifts your thinking from “I should break this case to prevent fall through to next” and shifts to “I’ll put a break before to prevent fall through from previous.”