1. 56
  1. 21

    Coding in C is like camping. It’s fun for a while, but eventually you really miss things like flushing toilets and grocery stores. You can fancy-up your sleeping bag + tent with a huge motor home/caravan by using C++. It’s like you have this house on wheels, but if you just want to take a sleeping bag + tent with you on a trip - you can. Take whatever you need.

    That being said… you could, and probably should (unless you’re doing embedded/retro-coding?), compile most C code with a C++ compiler, since C++ does enforce additional type safety/const correctness. The simple code examples in this article do not compile with a C++ compiler. Of course, C++ compilers cannot compile all C code due to this type safety so you’ll perhaps have to add casts to all your malloc/realloc/calloc functions or convert them to a modern C++ equivalent. Honestly if your goal is to reduce the mistakes you make in a C code base and you’re not interested in rewriting it in Zig or Rust, consider using a C++ compiler to compile it.

    1. 16

      I’m sorry, but I have C code, right now, that won’t compile with C++. There are features that are in C99 that aren’t in C++ yet, like designated initializers, or the ability to inline a structure inside a function call (I can never remember what this is called), and NULL. You can pry NULL from my cold dead hands, because it stands out way more than a bare 0 does (who thought that was a good idea for C++?). I do, however, crank the warnings up as high as they go and fix the warnings.

      1. 11

        You can pry NULL from my cold dead hands, because it stands out way more than a bare 0 does (who thought that was a good idea for C++?)

        Null pointers are complicated. In C, they are a horrible compromise because not all targets used a zero bit pattern to represent null. Any integer constant expression that evaluates to zero, and any such expression cast to any pointer type, are defined to be null pointers by the C spec. The following are all null pointers (or, at least, may be depending on context):

        0;
        (void*)0;
        1-1;
        (char*)(42 - 12 - 30);
        

        The following is not a null pointer:

        int x = 1;
        int *notNull = (int*)(x-1);
        

        Though in most implementations it will happen to have the same bit pattern as a null pointer and so will compare equal to one. Relying on this will bite you on some mainframes where the null pointer bit pattern is not 0 and address 0 is a valid address.

        There is also a macro in C called NULL that must be defined as a valid null pointer constant. Any of the examples above is a valid definition of this macro. Most implementations define it as (void*)0 because of one very painful corner case in C. Consider the following C function declaration and call:

        /// Append a null-terminated list of pointers to the container.
        void appendValues(struct SomeContainer *c, ...);
        
        appendValues(&container, &a, &b, &c, &d, NULL);
        

        On most 64-bit (LP64) C ABIs, if NULL is defined as 0 (which, remember, is a valid definition) then this will fail. The compiler will pass five pointers and one 32-bit integer to the variadic function and the high or low (depending on the target endian) 32 bits of the final pointer will be undefined and are likely to not be zero (this gets more likely as you add more arguments - in register the value may end up being sign extended, on the stack it will not and so you’ll read 4 bytes from outside of the argument frame).

        To prevent this kind of breakage, the null pointer macro must be defined with a cast to a pointer type (and I’ve never seen an implementation where it wasn’t). In C++, this introduced some difficulty. C permits implicitly casting from void* to any pointer type (which is a big part of the reason that the author of the article refers to it as ‘The Type Safety Is For Losers Language’). This means that it is completely valid to write:

        int *x = (void*)0;
        

        C++ does not have this escape hatch. If you’re doing dangerous things with pointers in C++ then you must do it explicitly. You can cast any pointer type to a void* (mostly for compatibility with C APIs) but the converse cast must be explicit, to highlight in the code that you’re doing a dangerous thing.

        In C++98, this meant that a definition of NULL as (void*)0 required a cast on every use. This was annoying and so the recommendation was to use 0 and an explicit cast to a pointer type if necessary (for examples, as in appendValues). This was not very satisfactory because in a language that at least sometimes pretends to be type safe, it’s a good idea if integers and pointers are not confused, even in the specific case of null.

        C++11 introduced nullptr and std::nullptr_t to address this. In C++11, null is not just some constant integer value, it is a singleton value, nullptr (which may, as with C, have any bit pattern representation) and it is of the type nullptr_t, which may be cast to any pointer type. Because it is a separate type, it also participates in overload resolution, which is useful for some compile-time checks because it allows you to specialise methods or templated functions differently if one parameter is known at compile time to be null (including static_asserting that it shouldn’t be).

        In most C++11 headers, the C NULL macro is defined to be nullptr and so NULL can be used in both languages.

        1. 2

          The following are all null pointers (or, at least, may be depending on context):

          0;
          (void*)0;
          1-1;
          (char*)(42 - 12 - 30);
          

          Some of these are not null pointers (0 and 1-1 are not, they don’t have pointer type), and some of these are not null pointer constants ((char *)(42 - 12 - 30) is not).

          There is also a macro in C called NULL that must be defined as a valid null pointer constant. Any of the examples above is a valid definition of this macro.

          Not quite. While your example (char*)(42 - 12 - 30) is a null pointer, it is not a null pointer constant, since it does not have integer type or type void *. If NULL had type char *, then you’d need an explicit cast every time you used an operator with NULL and a non-char pointer.

          Your example about variadic functions is a good one, but I think the takeaway is not that you should assume that implementations use (void *)0 as NULL, but that you must use an explicit cast when calling such functions (such as (char *)0 when using execl).

          1. 2

            Some of these are not null pointers (0 and 1-1 are not, they don’t have pointer type), and some of these are not null pointer constants ((char *)(42 - 12 - 30) is not).

            C11 Section 6.3.2.3, paragraph 4 says:

            An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant

            So you’re right about the char* version (though it is a null pointer constant cast to a different type) but not about 0 and 1-1. You are correct that the char* one isn’t a valid definition of NULL though.

            1. 2

              The definition of a null pointer is a null pointer constant converted to a pointer type. 0 and 1-1 are null pointer constants, but they have type int, so they are not null pointers.

              1. 2

                I don’t see that definition anywhere in the C11 spec. Can you give me a section and paragraph reference?

                1. 2

                  Sure, it’s the same paragraph you quoted earlier, C11 6.3.2.3p3:

                  If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

        2. 3

          Eh, what’s the problem with NULL in C++? I thought C++ standard library header cstddef defines NULL.

          1. 1

            C and C++ define NULL differently.

            In C, I think NULL is (void *)0 but in C++ it’s just 0. This is fixed in C++11 and higher with nullptr.

            1. 4

              How does nullptr fix anything for people who are targeting the common subset of C and C++ so that one can use C++ compiler to compile C? As I understand nullptr is not in C, is it?

              1. 7
                #ifdef __cplusplus
                # ifdef NULL
                #  undef NULL
                # endif
                # define NULL nullptr
                #endif
                
              2. 3

                In C, NULL is an implementation-defined null pointer constant, which is any integer constant expression with value 0, possibly cast to void *. So it could be 0, ((void *)0), or even something crazy like (1 - sizeof(char))

          2. 10

            The suggestion to take your C codebase and compile it at C++ doesn’t make sense. You can’t compile C as C++. You will need to port it. This will take a lot of work and the final result will not have the advantages of a program designed and implemented in C++ from the start - because it was originally written with C programming patterns. I do not think this approach is worth it. If you wanted to integrate C with C++ you can do that with linking, extern C etc.

            1. 4

              I’ll add it’s the same with Rust. You can run a C codebase through c2rust, but then you’re merely replacing gcc with rustc. For real improvements you need to refactor the code to use safe patterns.

              I’ve ported a few C libraries to Rust, and I found it’s hard. There’s a mismatch on a high level in how programs are architected. It’s normal for C programs to be pointer-heavy, rely on implicit sizes of buffers (a function gets a pointer to a buffer of unknown length, and just assumes it’s large enough), and vague runtime-dynamic memory ownership (you free some pointer only if some other flag is set, or just set it to null after free, so that it doesn’t crash when you try to free it for a second time).

              1. 2

                You will need to port it. This will take a lot of work and the final result will not have the advantages of a program designed and implemented in C++ from the start - because it was originally written with C programming patterns

                I half agree, having worked on some truly terrible C-to-C++-converted codebases. The minimal port is usually just adding a load of explicit casts, particularly on malloc calls. Turning the malloc and free into new and delete avoids these casts but requires you to do it everywhere (since allocating with new and freeing with free is UB - it might work, but if you’re allocating with new you might be tempted to add a destructor and then the free calls will deallocate but not destroy the objects).

                That said, if you write C that compiles with a C++ compiler, then you get C that has a lot more type checking at compile time. I’ve seen some folks do this (though not for over 10 years) for code that had to run on microcontrollers. They’d write it in the common subset of C and C++, compile as C++ on a PC for extra type checking, and then as C for deployment.

                These days, when every platform that has a C compiler but not a C++ compiler is a tiny embedded system that also lacks most of the C standard library, I can’t see any good reason for not just writing C++ and if you have a C codebase then incrementally transitioning it to C++ is easier than doing the same with any other language. You can keep the same struct definitions and extern "C" definitions for C APIs and gradually refactor internal things into idiomatic C++ one file and then one component at a time. A lot of the things in modern C++, such as std::unique_ptr are ways of expressing things in the type system that are implicitly part of the API contract in C, so you can move to using smart pointers internally and wrap them for the C interface, then remove the C interface when all callers are gone.

              2. 7

                Coding in C is like camping.

                Coding in C is like tightrope walking.

                It can be fun to get good at, and it can be very impressive to watch. But please don’t do it while you’re carrying my breakable stuff!

              3. 8

                By holding backwards compatibility and “not being a bother” as the highest ideals for C code, people who survive long enough in the industry begin to equate age with quality, as if codebases were barrels of wine in a cellar. The older and longer the code is used, the finer and more delectable the wine.

                Honestly this is a hard mindset to get rid of.

                Slightly related to the topic the other day on HN about the i32 Y2038 future dates bugs. The willingness the break things for us dev and admins, so that they don’t break for our actual users.

                1. 6

                  This is an amazingly passive aggressive writeup that not so subtly lays the blame for C’s tradition of not getting safer on the users and their desire to ignore and not fix bugs in their codebase when the compiler begins warning them about it.

                  I kind of love it for that reason.

                  1. 6

                    These are a bunch of great arguments in favor of not writing new code in C, and trying to port existing code in C to some other language that makes better correctness guarantees.

                    1. 3

                      The part where it says “for I am dog destroyer of all” messes up some screenreaders 💔