Live data from Hacker News

Some C habits I employ for the modern day

unix.dog

141–150 of 162 posts

Re: Some C habits I employ for the modern day

#141

Earlier quoted context omitted.

As I see it, the problem with languages trying to replace C is that they not only try to fix fundamental flaws, but feel compelled to add unneeded features and break C's simplicity.

Also they leave a important point of C behind: backward-compatibility.

D supports mixed C and D files in a project. The D code can call C functions and use C types, and the C code can call D functions using C types.

The D compiler will even translate C source code to D source code if you prefer! After using a mixed D/C program for a while, I bet you'll feel motivated to take the extra step and translate the C stuff to D, as the inelegance in C will become obvious :-)

Re: Some C habits I employ for the modern day

#143

Earlier quoted context omitted.

Your first example doesn't make sense, because struct S { int a; }; is also fine and idiomatic in C. It is rather typedef struct S { int a; } S; that doesn't make sense, because why would you make something opaque and expose it immediately again in the same line? The others are ... different. I can't tell whether they are really better. The second maybe, although I like it that the compiler forces me to forward type…

> is also fine and idiomatic in C It's inelegant because without the typedef, you need to prefix it always with `struct`. This is inelegant because all other types do not need a prefix. It also makes it clumsier to refactor the code (adding or subtracting the leading `struct`). The typedef workaround is extremely commonplace. > I like it that the compiler forces me to forward type stuff, it makes the code much more r…

> It's inelegant

We obviously disagree with the coding organization we prefer, so I find that rather elegant, but this doesn't sound like a substantial discussion. You as the language author are obviously quite content with the choices D made.

> This is inelegant because all other types do not need a prefix.

I don't find that. It makes it rather possible to clearly distinguish between transparent and opaque types. That these are a separate namespace makes it also possible to use the same identifier for the type and object, which is not always a good choice, but sometimes when there really is no point in inventing pointless names for one of the two, it really is. (So I can write struct message message; .) It also makes it really easy to create ad-hoc types, which honestly is my killer feature that convinced me to switch to C. I think this is the most elegant way to make creating new types for single use, short of getting rid of explicit types altogether.

> It also makes it clumsier to refactor the code (adding or subtracting the leading `struct`).

I never had that problem, and don't know when it occurs and why.

> The typedef workaround is extremely commonplace.

In my opinion that is not a workaround, but a feature. I also use typedefs when I want to declare an opaque type. This means that in the header file all function declarations refer to the opaque type, and in the implementation the type is only used with "struct". This also makes it obvious which types internals you are supposed to touch and which not. (This is also what e.g. the Linux style guide recommends.)

> This isn't what you want to see - you want to see first the public interface, not the implementation details.

Maybe you, but I don't. As in C public interface and implementation are split into different files, this problem doesn't occur. When I want to see the interface, I'm going to read the interface definition. When I look into the implementation file, I definitely don't expect to read the interface. What I rather see is first the dependencies (includes) and then the internal types. This fits "Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." . Then I typically see default values and configuration. Afterwards yes, I see the lowest methods.

> people tend to organize the code backwards, with the private leaf functions first and the public functions last.

Which results in a consistent organization. It also fits how you would write in in math or an academic context, that you only use what is already defined. It makes the file readable from top to bottom. When you are just looking for a specific thing, instead of trying to read it in full, you are searching and jumping around anyway.

> Oh, there is a looong list of kludgy problems stemming from a separate macro processor that is a completely distinct language from C. Even the expressions in a macro follow different rules than in C. If you've ever used a language with modules, you'll never want to go back to #include!

A macro language is surprising for the newcomer, but you get used to it, and I don't think there is a problem with include. Textual inclusion is kind of the easiest mental modal you can have and is easy to control and verify. Coming from a language with modules, before learning C, I never found that to be an issue, and rather find the emphasis on the bare filesystem rather refreshing.

> but in practice, why would one want a module name different from its filename?

True, I actually never wanted to include a file with spaces, but it is something where your concept breaks. Also you can write #include "foo/bar/../baz" just fine, and can even use absolute paths, if you feel like it.

Re: Some C habits I employ for the modern day

#144

Earlier quoted context omitted.

> is also fine and idiomatic in C It's inelegant because without the typedef, you need to prefix it always with `struct`. This is inelegant because all other types do not need a prefix. It also makes it clumsier to refactor the code (adding or subtracting the leading `struct`). The typedef workaround is extremely commonplace. > I like it that the compiler forces me to forward type stuff, it makes the code much more r…

> It's inelegant We obviously disagree with the coding organization we prefer, so I find that rather elegant, but this doesn't sound like a substantial discussion. You as the language author are obviously quite content with the choices D made. > This is inelegant because all other types do not need a prefix. I don't find that. It makes it rather possible to clearly distinguish between transparent and opaque types. Th…

> macro language is surprising for the newcomer, but you get used to it

This was one of the biggest paradigm shifts for me in mastering C. Once I learned to stop treating the preprocessor as a hacky afterthought, and realized that it's actually a first-class citizen in C and has been since its conception, I realized how beautiful and useful it really is when used the way the designers intended. You can do anything with it, literally anything, from reflection to JSON and YAML de/serialization to ad hoc generics. It's so harmonious, if unsightly, like the fat lady with far too much makeup singing the final opus.

Re: Some C habits I employ for the modern day

#145

Earlier quoted context omitted.

> It's inelegant We obviously disagree with the coding organization we prefer, so I find that rather elegant, but this doesn't sound like a substantial discussion. You as the language author are obviously quite content with the choices D made. > This is inelegant because all other types do not need a prefix. I don't find that. It makes it rather possible to clearly distinguish between transparent and opaque types. Th…

> macro language is surprising for the newcomer, but you get used to it This was one of the biggest paradigm shifts for me in mastering C. Once I learned to stop treating the preprocessor as a hacky afterthought, and realized that it's actually a first-class citizen in C and has been since its conception, I realized how beautiful and useful it really is when used the way the designers intended. You can do anything wi…

> You can do anything with it, literally anything, from reflection to JSON and YAML de/serialization to ad hoc generics.

Wow. Do you have any pointers? I always thought random computation with it is hard, because it doesn't really wants to do recursion by design. Or are you talking about using another program as the preprocessor?

Re: Some C habits I employ for the modern day

#146

> and I end up having all these typedefs in my projects I avoid doing this now. It's more trouble than it's worth and it changes your code from a standard dialect of C into a custom one. Plus my eyes are old and they don't enjoy separating short identifiers. > typedef struct { ... } String I avoid doing this. Just use `struct string { ... };'. It makes it clear what you're handling. C23 finally gave us "auto", you sh…

> So you use strlen() a lot and don't have to deal with multibyte characters anywhere in your code. It's not much of a strategy. You don't need to support all multibyte encodings (i.e. DBCS, UCS-2, UCS-4, UTF-16 or UTF-32) characters if you're able to normalise all input to UTF-8. I think, when you are building a system, restricting all (human language) input to be UTF-8 is a fair and reasonable design decision, and…

I do not agree that restricting it to UTF-8 (or to Unicode in general) is a fair and reasonable design decision (although UTF-8 may be reasonable if Unicode is somehow required anyways (you should avoid requiring Unicode if you can though), especially the program is also expected to deal with ASCII in addition to requiring Unicode), but regardless of that, the number of code points is not usually relevant (and substring operations indexed by code points is not usually necessary either), and the number of bytes will be more important, and some programs should not need to know about the character encoding at all (or only have a limited consideration of what they do with them).

(One reason you might care about the number of code points is because you are converting UTF-8 to UTF-32 (or Shift-JIS to TRON-32 or whatever else) and you want to allocate the memory ahead of time. The number of characters (which is not the same as the number of code points in the case of Unicode, although for other character sets it might be) is probably not important; if you want to display it, you will care about the display width according to the font, and if you are doing editing then where one character starts and ends is going to be more significant than how many characters they are. If you are using and indexing by the number of code points a lot (even though as I say that should not usually be necessary), then you might use UTF-32 instead of UTF-8.)

(It is also my opinion that Unicode is not a good character set.)

Re: Some C habits I employ for the modern day

#147
post #44

Earlier quoted context omitted.

C++ has many things, and that is why many programmers want to stick with C

if you don't like those things, then don't use them

There are also some things in C that do not work or work differently in C++, such as (void*), empty structures (which in C++ are not really empty), etc; and there is also such C++ stuff such as name mangling, the C++ standard library, etc, even if those things are not a part of your program, which is another reason why you might prefer C.

Re: Some C habits I employ for the modern day

#148

Earlier quoted context omitted.

> is also fine and idiomatic in C It's inelegant because without the typedef, you need to prefix it always with `struct`. This is inelegant because all other types do not need a prefix. It also makes it clumsier to refactor the code (adding or subtracting the leading `struct`). The typedef workaround is extremely commonplace. > I like it that the compiler forces me to forward type stuff, it makes the code much more r…

> It's inelegant We obviously disagree with the coding organization we prefer, so I find that rather elegant, but this doesn't sound like a substantial discussion. You as the language author are obviously quite content with the choices D made. > This is inelegant because all other types do not need a prefix. I don't find that. It makes it rather possible to clearly distinguish between transparent and opaque types. Th…

Can you justify C rejecting the following code:

    int *p, *q;
    auto x = p * q;

?

Re: Some C habits I employ for the modern day

#149

Earlier quoted context omitted.

> It's inelegant We obviously disagree with the coding organization we prefer, so I find that rather elegant, but this doesn't sound like a substantial discussion. You as the language author are obviously quite content with the choices D made. > This is inelegant because all other types do not need a prefix. I don't find that. It makes it rather possible to clearly distinguish between transparent and opaque types. Th…

> macro language is surprising for the newcomer, but you get used to it This was one of the biggest paradigm shifts for me in mastering C. Once I learned to stop treating the preprocessor as a hacky afterthought, and realized that it's actually a first-class citizen in C and has been since its conception, I realized how beautiful and useful it really is when used the way the designers intended. You can do anything wi…

D accomplishes this by using Compile Time Function Execution to build D source code from strings, and then inline compiling the D code. Learning a macro language is unnecessary, as it's just more D code.

This kind of thing is very popular among D users.

https://dlang.org/spec/statement.html#mixin-statement

https://dlang.org/spec/expression.html#mixin_expressions

Re: Some C habits I employ for the modern day

#150
post #67

I'm a huge fan of the 'parse, don't validate' idiom, but it feels like a bit of a hurdle to use it in C - in order to really encapsulate and avoid errors, you'd need to use opaque pointers to hidden types, which requires the use of malloc (or an object pool per-type or some other scaffolding, that would get quite repetitive after a while, but I digress). You basically have to trade performance for correctness, wherea…

> But then anyone could just instantiate an invalid Name without calling the parse_name function and pass it around wherever This is nothing new in C. This problem has always existed by virtue of all struct members being public. Generally, programmers know to search the header file / documentation for constructor functions, instead of doing raw struct instantiation. Don‘t underestimate how good documentation can driv…

Sometimes you want the struct to be defined in a header so it can be passed and returned by value rather than pointer.

A technique I use is to leverage GCC's `poison` pragma to cause an error if attempting to access the struct's fields directly. I give the fields names that won't collide with anything, use macros to access them within the header and then `#undef` the macros at the end of the header.

Example - an immutable, pass-by-value string which couples the `char*` with the length of the string:

    #ifndef FOO_STRING_H
    #define FOO_STRING_H
    
    #include 
    #include 
    #include 
    #include "config.h"
    
    typedef size_t string_length_t;
    #define STRING_LENGTH_MAX CONFIG_STRING_LENGTH_MAX
    
    typedef struct {
        string_length_t _internal_string_length;
        char *_internal_string_chars;
    } string_t;
    
    #define STRING_LENGTH(s) (s._internal_string_length)
    #define STRING_CHARS(s) (s._internal_string_chars)
    
    #pragma GCC poison _internal_string_length _internal_string_chars
    
    constexpr string_t error_string = { 0, nullptr };
    constexpr string_t empty_string = { 0, "" };
    
    inline static string_t string_alloc_from_chars(const char *chars) {
        if (chars == nullptr) return error_string;
        size_t len = strnlen(chars, STRING_LENGTH_MAX);
        if (len == 0) return empty_string;
        if (len 
It just wraps `` functions in a way that is slightly less error prone to use, and adds zero cost. We can pass the string everywhere by value rather than needing an opaque pointer. It's equivalent on SYSV (64-bit) to passing them as two separate arguments:

    void foo(string_t str);
    //vs
    void foo(size_t length, char *chars); 
These have the exact same calling convention: length passed in `rdi` and `chars` passed in `rsi`. (Or equivalently, `r0:r1` on other architectures).

The main advantage is that we can also return by value without an "out parameter".

    string_t bar();
    //vs
    size_t bar(char **out_chars);
These DO NOT have the same calling convention. The latter is less efficient because it needs to dereference a pointer to return the out parameter. The former just returns length in `rax` and chars in `rdx` (`r0:r1`).

So returning a fat pointer is actually more efficient than returning a size and passing an out parameter on SYSV! (Though only marginally because in the latter case the pointer will be in cache).

Perhaps it's unfair to say "zero-cost" - it's slightly less than zero - cheaper than the conventional idiom of using an out parameter.

But it only works if the struct is That aside, when we define the struct in the header we can also `inline` most functions, so that avoids unnecessary branching overhead that we might have when using opaque pointers.

`#pragma GCC poison` is not portable, but it will be ignored wherever it isn't supported, so this won't prevent the code being compiled for other platforms - it just won't get the benefits we get from GCC & SYSV.

The biggest downside to this approach is we can't prevent the library user from using a struct initializer and creating an invalid structure (eg, length and actual string length not matching). It would be nice if there were some similar to trick to prevent using compound initializers with the type, then we could have full encapsulation without resorting to opaque pointers.

Post reply on HN