I wouldn’t sell yourself short. I think it is a lot more difficult to start from a clean slate and create something which hasn’t existed before. When I personally start from scratch it shows in the code, things are messy, ideas evolve, and the target changes.

I’ve also had the privilege of working with other people improving/refining stuff that I’ve written. With the large picture already established they can focus their energy in areas which I simply couldn’t. They apply experience that I lacked. The end result was (nearly) always better.

9 Likes

One thing I’ve learned to love is not commenting out code but #if 0’ing it out. Two reasons: first, anytime you identify a bit of code you’re going to change, #if 0 it out, #else your new changes. Now testing your new implementation against the old one–which I always find I wished I hadn’t deleted, and don’t want to go git-wrangling to get it back–is just flipping that condition. Clean up after yourself once done. This is the one thing that makes me want to jam the c preprocessor onto everything.

The second reason is maybe more sinful, but using it as an actual alternative to commented out code. Commented out code is always a bit weird–always seems to be in the wrong place, an artefact from an earlier revision, whatever. But #if fixes it in place: it’s not a hazy ghost from the past, switching it on should enable or change some feature, and it should compile and work. Especially good for debug code that you keep rewriting every time you revisit a bit of code. Of course this requires discipline, but so much of programming boils down to discipline anyway. I should add that I’m otherwise totally opposed to the horrible interleaving of implementations controlled by compile flags–blegh–but this is ok, in moderation

4 Likes

I also like to use #if 0. Though usually it’s for a very brief period of time while I test a small chunk of code. I don’t like them to survive a commit.

3 Likes

If you never take the address, then use the static version. Limiting scope is also limiting cognitive overhead so I think it’s best to do so whenever possible.

3 Likes

Yeah, it gets confusing quickly if it’s committed. That’s why I mentioned I don’t like to actually commit the defines. Once I decide if I prefer the code with or without the #if 0 section I will delete the define. If there’s a few different ways to do something I prefer to contain them in their own functions or whatnot and then use a runtime flag to decide which to use. I’ve found that approach is less applicable to designing synths and whatnot but it was very useful when deploying networked applications to be able to turn features on and off or try them on a subset of machines.

A big problem with commented-out code or #if 0 blocks that survive for any length of time is that they tend to rot. Since the code is not being compiled or run, its behaviour will fall out of line with the rest of the program as it evolves.

4 Likes

So with static variables, should one consider defining them at the top of the function even if a narrower scope would suffice? I’m trying to put myself in the shoes of someone reading the code for the first time, would they want to know upfront…? I think I would.


Absolutely, I often find myself using GitHub’s blame / history UI. It’s surprisingly good.

Even having #if DEBUG in your code can cause all manor of problems, you end up with different bugs in each of your code paths. There’s a really annoying heisenbug in the Teletype USB memory stick code, turn the debug statements on in the MIDI code and it works, turn them off and it doesn’t.

1 Like

I usually put them at the top. If it’s still confusing, the function is probably too big / complex and could be split up. The most important things for me when reading code are: a) where did that variable come from? and b) what changes this variable? Narrow scope and smaller functions make both of those things easier to determine :slight_smile:

3 Likes

So, it’s a timing thing then, right? Isn’t that what heisenbugs generally are?

2 Likes

Not necessarily timing, could also be concurrency or just global state being mutated in an unexpected way.

2 Likes

I hear you… but then I don’t want to derail this thread :wink:

1 Like

Yep, as @kisielk says it could be other things, I’m not sure what the internals of the AVR32 are, but there may be interrupts generated on the serial and USB ports, and thus we might be altering the sequence of events.

In any case, the Atmel USB framework expects you to wait until it’s told you via callback that the USB stick is available. We’re not doing that now, so the fix is really to rewrite the code to do it that way, which will have the added benefit of also allowing saving and loading at any time and not just at boot.

2 Likes

Two things:

a) I started programming before C allowed variables to be declared anywhere but the top of the function (or block), so by old habits I put everything at the top. This may not seem like much of a reason, but your code will be portable to extremely old compilers, which can be common in the embedded world.

b) A great rule of thumb is to make functions small enough that they fit on the page in your editor and can be grokked in a single viewing. In this sense, you might as well put them at the top because there isn’t going to be much of a “middle” in the function anyway.

1 Like

Elder, yea. But the modern way (4.2BSD, POSIX.1) is source first.

include <strings.h>

void bcopy(const void *s1, void *s2, size_t n);

The bcopy() function copies n bytes from string s1 to string s2. The two
strings may overlap. If n is zero, no bytes are copied.

Ah, I think we’d have to disagree on this point. Smallest scope possible unless you have a good reason. IMO there is a reason why it was added to the standard.

I can’t see myself wanting to work with proprietary C compilers anyway, as I’m not paid to do this.

Definitely!

2 Likes

Just need to put this out there.

Screw you C

Had some weird bugs with the teletype beta. If I add -fno-common to the compiler flags I get a few errors in libavr32, will discuss them in the 2.0 beta thread once I get some time to write it up.

Anyway once more with feeling.

Screw you C

3 Likes

Really, really awesome tip! Seems that multiple definitions can arise when you forget to include the extern keyword in global variable declarations inside a header, when that header is #include-ed by several .o files.

Kind of makes me realise I still don’t fully understand all the implications/ramifications/pitfalls of variable declaration in C:

  • inside header, inside .c file or both?
  • what’s the ‘default’ behaviour when you don’t declare extern/static?
  • program compiled via .o files, vs all .c files compiled to executable in one go?
  • rule of thumb for when to use the volatile keyword?
  • anything else that’s gone under the radar? (e.g this multiple definition thing)

EDIT:
Some bedtime reading/revision:
http://publications.gbdirect.co.uk/c_book/chapter8/

There are a number of murky and convoluted backwaters left unexplored on grounds of sympathy and compassion for the sufferer, and some without any better home. This chapter gathers them together—it’s the toxic waste dump for the nasty bits of C.

1 Like

That I knew about.

This is what happened to me… I had 2 global variables with the same name and compatible types in .c files only!

  • bool dirty in one
  • uint8_t dirty in another

Somehow it turns out that these should refer to the same location in memory and are the same variable. This is the ‘common model’, which can be disabled with --fno-common. The same would apply if they were both the exact same type too (I had quite a few of them too).

We think of header files are somehow being special, when they aren’t really part of the compiler as such, but more part of the preprocessor. The compiler/linker sees 2 variables with the same name and type but no static / extern qualifier and has to make a guess as to what to do, by the time you’re at the linking stage the preprocessing stage is long gone.

Nonetheless, I can’t think of any good reason why a programmer would deliberately choose to declare extern variables in this way, rather it’s that they’ve forgotten the static qualifier. IMO when the compiler is faced with such an ambiguity it should print out a warning.

For the life of me I can’t remember the thought process that lead me to start Googling “same variable name in different c files”, especially as there is no debugger available. I do remember having to persevere and try multiple permutations of the question before I got some results.

4 Likes

Afaik if you have no definition, declarations are extern by default. That’s because there’s no definition there, so it must assume it’s defined elsewhere unless told otherwise.

1 Like

Going by the excellent cppreference.com (from the C language section), there appears to be a ‘tentative’ variable declaration, so:

  • int i = 3 is assumed to be extern
  • int i is assumed to be ‘tentative’

Quoting from the same:

A tentative definition is a declaration that may or may not act as a definition. If an actual external definition is found earlier or later in the same translation unit, then the tentative definition just acts as a declaration.

It’s definitely a bit confusing. Or at least I find it so.

The GCC docs for -fno-common are also useful.

-fno-common
In C code, this option controls the placement of global variables defined without an initializer, known as tentative definitions in the C standard. Tentative definitions are distinct from declarations of a variable with the extern keyword, which do not allocate storage.

Unix C compilers have traditionally allocated storage for uninitialized global variables in a common block. This allows the linker to resolve all tentative definitions of the same variable in different compilation units to the same object, or to a non-tentative definition. This is the behavior specified by -fcommon, and is the default for GCC on most targets. On the other hand, this behavior is not required by ISO C, and on some targets may carry a speed or code size penalty on variable references.

The -fno-common option specifies that the compiler should instead place uninitialized global variables in the data section of the object file. This inhibits the merging of tentative definitions by the linker so you get a multiple-definition error if the same variable is defined in more than one compilation unit. Compiling with -fno-common is useful on targets for which it provides better performance, or if you wish to verify that the program will work on other systems that always treat uninitialized variable definitions this way.

There is more information out there if you you Google “-fno-common”, there are some links that discuss what the linker does and the .COMMON block.

Personally I think I will enable -fno-common on all my C projects (not that I have any).

(FYI, C++ doesn’t have this issue.)

1 Like

Yet another reason to use C++ rather than some antiquated language :wink:

2 Likes