return to table of content

You've just inherited a legacy C++ codebase, now what?

eschneider
81 replies
21h39m

Some good advice here, and some more...controversial advice here.

After inheriting quite a few giant C++ projects over the years, there are a few obvious big wins to start with:

* Reproducible builds. The sanity you save will be your own. Pro-tip: wrap your build environment with docker (or your favorite packager) so that your tooling and dependencies become both explicit and reproducable. The sanity you save will be your own.

* Get the code to build clean with -Wall. This is for a couple of reasons. a) You'll turn up some amount of bad code/undefined behavior/bugs this way. Fix them and make the warning go away. It's ok to #pragma away some warnings once you've determined you understand what's happening and it's "ok" in your situation. But that should be rare. b) Once the build is clean, you'll get obvious warnings when YOU do something sketchy and you can fix that shit immediately. Again, the sanity you save will be your own.

* Do some early testing with something like valgrind and investigate any read/write errors it turns up. This is an easy win from a bugfix/stability point of view.

* At least initially, keep refactorings localized. If you work on a section and learn what it's doing, it's fine to clean it up and make it better, but rearchitecting the world before you have a good grasp on what's going on globally is just asking for pain and agony.

dataflow
40 replies
21h4m

Step 0: reproducible builds (like you said)

Step 1: run all tests, mark all the flaky ones.

Step 2: run all tests under sanitizers, mark all the ones that fail.

Step 3: fix all the sanitizer failures.

Step 4: (the other stuff you wrote)

hyperman1
21 replies
20h8m

If we're going to visit the circles of hell, let's do it properly:

Step -1: Get it under source control and backed up.

Step -2: Find out if the source code corresponds to the executable. Which of the 7 variants of the source code (if any).

Step -3: Do dark rituals over a weekend with cdparanioa to scrape the source code from the bunch of scratched cd's found in someone's bottom drawer. Bonus point if said person died last week, and other eldritch horrors lurk in that bottom drawer. Build a VM clone of the one machine still capable of compiling it.

Yes, I have scars, why do you ask?

skissane
5 replies
15h0m

Question: Why does some of the product source code look like it is the output of a decompiler?

Answer: Our office was in the WTC and was destroyed on 9/11. Luckily everyone got out alive, but then we discovered we had no off-site backups of the source code. In order to continue development, we had to retrieve the released binaries from our customers and decompile them to get back source code.

guappa
2 replies
9h6m

I see this as a case of: "see, allowing wfh would have saved you there."

skissane
0 replies
7h5m

From what I was told, WFH and people starting late actually really helped, in that there were only a handful of people physically in the office when it happened. But apparently the server with the source code on it was in it too. I don’t know if they forgot about off-site backups entirely, or if they thought they had them but only discovered they were incomplete or faulty afterwards

I don't know what the practice was at that time, but some years later, people weren't allowed to have the source on their laptops, they had to SSH/RDP/etc in to a hosted development system to work on it, which might explain how losing the office resulted in losing the source code even with people doing WFH

bluGill
0 replies
3h44m

No it wouldn't have - 2001 was 4 years before distributed version control. Work from home might have had current copies of some branches checked out on local machines, but nobody would have had full history or code from release branches on their machine.

fransje26
1 replies
9h7m

Oh.. Ouch, ouch, ouch. I feel for you. That must have been hell.

skissane
0 replies
7h8m

I wasn’t there for 9/11, my involvement with that code base started a few years later. But it was clear it was a traumatic memory for colleagues who had been

galangalalgol
3 replies
19h58m

I was assuming it already had unit and system tests with decent coverage. I forgot how bad stuff gets.

Maybe VM clones of various users too, and recordings of their work flows?

Cerium
2 replies
19h12m

I'm always careful to dump the bash history as soon as I get access to a machine involved in a legacy project.

eschneider
0 replies
18h20m

Oooh, smart!

cqqxo4zV46cp
0 replies
9h33m

Yep! Learned this one the easy way! I don’t get to say that often, so I’m taking full advantage.

skipkey
2 replies
15h29m

Everyone who has been around this game for long enough has scars. At one point in the early 90s I was asked to take over maintenance of a vertical market accounting app that had a few hundred users. It had been written using Lattice C, but at the time was being built with the ultra modern MS C 5.1.

The first time I looked at it, I saw that the make file set the warning level to 0 and redirected the output to NUL. Removing that, I ran nmake and prayed. About 45 minutes later it finished building, evidently successfully. It turned out that having the warnings actually print added 10 minutes to the build.

It was averaging more than one error per line of code at /w3. And it was around 40k lines of code. Not large by the standards of today but huge back then for MSDOS. Peeking, it used K&R c and included no header files. So step 1 for me was to hook up some scaffolding using various tools from mks to make sure as I edited things that the error count didn’t increase.

The biggest thing I learned from this project was to not combine other coding with the warning removal or other cleanup. Makes it much easier to spot when you introduce bugs.

rightbyte
1 replies
7h23m

Was there a warning for implicit function declaration and implicit variable type for each variable and call that used those? Or how could there be that many warnings.

skipkey
0 replies
6h19m

Yes, plus a bunch of warnings for unsafe type conversions where the compiler did pointer to int, back to pointer conversions. In the process of cleaning it up I found at least a dozen serious bugs.

eschneider
2 replies
19h27m

There was that time when I had to dump the roms off a 'test' MRI machine because that's the only version of the code they had, then decompiled it, and rewrite it from that.

I think about that a lot now that I'm older and spend a fair bit of time in MRI machines...

reactordev
1 replies
16h59m

Dang man that’s tough, at least you know they work ;)

eschneider
0 replies
4h3m

Oh, we tested those a lot. Fun fact: you can see under the foil in scratch cards with those. Might, um, have something to do with "every card a winner" scratch cards no longer being a thing.

thrwwycbr
1 replies
14h14m

Shouldn't it be step -3 to -1?

hyperman1
0 replies
11h39m

Of course not. You try to do 0 but that's impossible because you need to do -1 first. So you drop everything, try to do -1 but that's ... It's yak shaving's evil twin!

ta2234234242
1 replies
14h45m

Some people believe that if you read the C++ standard recreationally, it should be interpreted as a call for help, and intervention is required, putting the subject under 24/7 monitoring and physical restraints.

/s

Step -4: Get the version of windows and the compiler it was last known to compile with.

fransje26
0 replies
9h0m

Step -3.5: Do the service pack and .net framework update dance, wave a dead chicken, and hopefully, by shear luck, install them in the correct order. If not, uninstall and goto -4;

Been there. Done that.

denkmoon
0 replies
18h41m

This gave me a good laugh. I too have been here.

groby_b
10 replies
16h44m

Step 0 sounds so easy. Until you realize __time__ exists. Then you take that away, and you find out that some compiler heuristics might not be deterministic.

Then you discover -frandom-seed - and go ballistic when you read "but it should be a different seed for each sourcefile"

Then you figure out your linker likes emitting a timestamp in object files. Then you discover /Brepro (if you're lucky enough to use lld-link.

Then you used to discover that Win7's app compat db expected a "real" timestamp, and a hash just won't do. (Thank God, that's dead now). This is usually the part where you start questioning your life choices.

Then somebody comes to your desk and asks if you can also make partial rebuilds deterministic.

On the upside, step 1 is usually quick, there will be no tests.

dataflow
7 replies
16h24m

I think (well, assumed) what they meant by deterministic builds was merely hermetic builds, which are easier. True determinism is overkill for step 0.

groby_b
4 replies
16h20m

Often yes. Sometimes, no. You haven't enjoyed C++ until you get reports of the app intermittently crashing, and your build at the same version just won't.

But yes, if the goal is "slap it all in a container", that's probably good and at least somewhat reproducible. We aren't Python here! ;)

eschneider
1 replies
3h58m

That's probably reading uninitialized memory. You can get away with that for a VERY long time, until you can't. See the earlier valgrind recommendation.

But that sort of report isn't a deep mystery, it's just a specific class of bug. Given the description, you've got a pretty good idea of what you're looking for.

groby_b
0 replies
40m

... until the cause is really and truly a non-deterministic build. Trust me, been there.

For a long-ago example: I worked on a project that had an optimizer that used time-bounded simulated annealing to optimize. No two builds ever the same. It was "great".

boolemancer
1 replies
15h45m

Often yes. Sometimes, no. You haven't enjoyed C++ until you get reports of the app intermittently crashing, and your build at the same version just won't.

That's okay, it's probably just some bank in a random country that requires some software package to be installed, presumably in the interest of security, which injects a dll into every process on the machine and unsurprisingly has a bug which causes your process to crash at random in only that part of the world.

cesarb
0 replies
3h58m

some software package to be installed, presumably in the interest of security, which injects a dll into every process on the machine

You don't even have to get that far. Shell extensions (for file open or save dialogs) and printer drivers also introduce arbitrary DLLs to your processes. And some of them are compiled in an old version of IIRC Delphi or Turbo Pascal, which on the DLL startup code unconditionally changes the floating point control word to something which causes unexpected behavior in some framework you're using.

(We ended up wrapping all calls to file open or save or print dialogs with code to save and restore the floating point control word, just in case they had loaded one of these annoying DLLs.)

rfoo
1 replies
12h26m

Agreed.

Though in the last 6 years I've seen at least one case where truly deterministic builds mattered:

A performance bug only happens when a malloc() during init was not aligned to 32 bytes, glibc on x86_64 only guaranteed 16 bytes, but depending on what alloc / dealloc happened before it may just land on 32 bytes boundary.

The alloc / dealloc sequence before that point was pretty deterministic, however there were a few strings containing __FILE__. And gitlab runner checked-out codes to a path with random number (or an index? I don't remember) without -ffile-prefix-map or $PWD trick so its length varies.

rightbyte
0 replies
7h16m

It is really nice to have determinatistic builds when doing estetic clean ups, to verify that the code does not change, or inspecting changes in the assembly code and limit the scope of change to just the affected code.

peteradio
0 replies
3h54m

Then somebody comes to your desk and asks if you can also make partial rebuilds deterministic.

This is a good guy. Knows what they need, knows you are smart enough to potentially finally slay the dragon, will fight the bureaucracy on your behalf. Asking a hard ask is rarely beneficial for the asker on the failure side. Don't burn yourself out for it though and don't be afraid to ask hard favors from the asker.

nightpool
0 replies
15h47m

yeah I don't think OP is talking about byte perfect determinism, they just want CI not to explode. that's the triage goal, byte perfect determinism is not your first priority when stopping the bleeding on a legacy c++ project

eschneider
5 replies
20h57m

Just a note on legacy tests: Step 0.5: understand the tests. They need to be examined to see if they've rotted or not. Tests passing/failing doesn't really mean code under test works or not. The tests might have been abandoned under previous management and don't accurately reflect how the code is _supposed_ to be working.

roland35
0 replies
2h32m

I find it helpful to try and intentionally break the code under test.

Sometimes the test still passes, and that is a good sign that something is very wrong!

mst
0 replies
2h37m

The same applies to comments.

I have absolutely inherited codebases where one of the early steps was to make a commit excising every single comment in the code, because so many of them were old, lies, or old lies, that it wasn't worth the risk of a junior developer accidentally thinking they could be relied upon.

(and of course they remained available in history to be potentially buffed up and resurrected, but still, argh)

fransje26
0 replies
8h59m

Look at mister fancy here, having tests in his legacy code base.

dataflow
0 replies
18h43m

I'd put that under 2.5 or 3.5, if not later. You only really need to do it before you start modifying code, and it's a massive effort to understand a new codebase. Better pick the lower-hanging fruit (like corruption bugs) so you can at least stay sane when you run the tests and try to understand them.

Zobat
0 replies
2h21m

My project has decent code, source control with history from the beginning (ten years in a few months) and unit tests that were abandoned for years. I've spent at least a couple of weeks, over a year or so, just to remove tests that didn't test current functionality and get the others to work/be green. They ain't fast and only semi stable but they regularly find bugs we've introduced.

daemin
0 replies
20h20m

Probably insert another Step 1: implement tests Be they simple acceptance tests, integration tests, or even unit tests for some things.

microtherion
28 replies
20h39m

Get the code to build clean with -Wall.

This is fine, but I would strongly recommend against putting something like -Wall -Werror into production builds. Some of the warnings produced by compilers are opinion based, and new compiler versions may add new warnings, and suddenly the previous "clean" code is no longer accepted. If you must use -Werror, use it in debug builds.

ladberg
19 replies
20h34m

This is fixed by the suggestion right before it:

* Reproducible builds. The sanity you save will be your own. Pro-tip: wrap your build environment with docker (or your favorite packager) so that your tooling and dependencies become both explicit and reproducable. The sanity you save will be your own.

Upgrading compiler versions shouldn't be done out-of-band with your normal PR process.

jchw
18 replies
20h32m

I agree that this helps, although I still think that in general, the default build should never do -Werror, since people may use other toolchains and it shouldn't surprise-break downstream (I'm pretty sure this is a problem Linux distros struggle with all the time..) If it does it only in your fully reproducible CI, then it should be totally fine, of course.

eschneider
9 replies
20h27m

The scripted, packaged docker with toolchain dependencies and _is_ the build. If someone decides to use a different toolchain, the problems are on them.

jchw
7 replies
20h21m

Yeah that works if you are not dealing with open source. If you are dealing with open source, though, it really won't save you that much trouble, if anything it will just lead to unnecessarily hostile interactions. You're not really obligated to fix any specific issues that people report, but shrugging and saying "Your problem." is just non-productive and harms valuable downstreams like Linux distributions. Especially when a lot of new failures actually do indicate bugs and portability issues.

nicoburns
2 replies
17h27m

C++ is super annoying in this way. Many other languages (e.g Rust) only have one compiler and good portability out of the box which completely avoids this problem. And other ecosystems that do have multiple implementation (e.g. JavaScript) seem to have much better compatibility/interop such that it's not typically a problem you have to spend much if any time on in practice.

jxramos
0 replies
9h38m

I'm curious what sort of CPUs and OSes do those languages run on. C++ runs on all sorts of obscure real time OSes, all the standard mainstream ones as well as on embedded equipment and various CPUs, but a lot of that is possible because of the variety of compilers.

adastra22
0 replies
9h55m

I’ve had rust projects with strict clippy rules break when rustc is upgraded.

aseipp
1 replies
16h26m

Supporting every Linux distribution and their small differences isn't free, and Linux distributions shipping things you haven't tested directly is also a way for users to get bitten by bugs or bad interactions, which they will then report to you directly anyway so you're responsible for it. It's complicated. It's happened plenty of times where e.g. I've run into an obscure and bad bug caused by a packaging issue, or a downstream library that wasn't tested -- or there's a developer who has to get involved with a specific distro team to solve bugs their users are reporting directly to them but that they can't reproduce or pinpoint, because the distro is different from their own environment. Sometimes these point out serious issues, but other times it can be a huge squeeze to only get a little juice.

For some things the tradeoffs are much less clear, open-source or not e.g. a complex multi-platform GUI application. If you're going to ship a Flatpak to Linux users for example, then the utility of allowing any random build environments is not so clear; users will be running your produced binaries anyway. These are the minority of cases, though. (No, maybe not every user wants a Flatpak, but the developers also need to make decisions that balance many needs, and not everything will be perfect.)

Half of the problem, of course, is C and C++'s lack of consistent build environments/build systems/build tooling, but that's a conversation for another day.

That said, I generally agree with you that if you want to be a Good Citizen in the general realm of open-source C and C++ code, you should not use -Werror by default, and you should try (to whatever reasonable extent) to allow and support dependencies your users have. And try to support sanitizers, custom CFLAGS/CXXFLAGS, allow PREFIX and DESTDIR installation options, obey the FHS, etc etc. A lot of things have consolidated in the Linux world over the years, so this isn't as bad as it used to be -- and sometimes really does find legitimate issues in your code, or even issues in other projects.

jchw
0 replies
15h5m

Again, you don't have to fix bugs that are reported, but treating it as invalid to use any compiler versions except for the exact ones that you use is just counterproductive.

The "utility" of allowing "any random build environment" is that those random build environments are the ones that exist on your user's computers, and absent a particularly good reason why it shouldn't work (like, your compiler is too old, or literally broken,) for the most part it should, and usually, it's not even that hard to make it work. Adopting practices like defaulting -Werror -Wall on and closing bugs as WONTFIX INVALID because it's not any of the blessed toolchains gains you... not sure. I guess piece of mind from having less open issues and one less flag in your CI? But it is sure to be very annoying to users who have fairly standard setups and are trying to build your software; it's pretty standard behavior to report your build failures upstream, because again, usually it does actually signal something wrong somewhere.

Developers are free to do whatever they want when releasing open source code. That doesn't mean that what they are doing is good or makes any sense. There are plenty of perfectly legal things that are utterly stupid to do, like that utterly bizarre spat between Home Assistant and NixOS.

saagarjha
0 replies
9h18m

It doesn't even work outside of open source. I am running a prerelease toolchain almost all the time on my computer. If the project at work turns on -Werror, I immediately turn it off and store away the change. Of course this means that I send in code fixes for things that don't reproduce on other people's machines yet, but I literally never receive pushback for this.

michaelmior
0 replies
6h23m

I would say it's still worth having -Werror for some "official" CI build even if it is disabled by default.

dotnet00
0 replies
3h43m

Open source projects that insist their docker container is the only way to go are going to be an instant reject from me. It's a total copout to just push a docker container and insist that anyone not using it is on their own.

Docker is too fraught with issues for that, and as anyone can attest, there are few things more frustrating in computing than having to follow down a chain of chasing issues in things only superficially related to what you actually want to do.

The least that can be done is for the project to do its best to not be dependent on specific versions, and explicitly document, in a visible place, the minimum and maximum versions known to work, along with a last changed date.

charcircuit
5 replies
16h46m

Changing other dependencies can also cause the build to break. The best thing to do is to use the dependencies the project specifies.

jchw
4 replies
14h52m

Technically changing literally anything, including the processor microarchitecture that the developer originally tested the code on, could easily cause a real-world breakage. That doesn't mean it should, though.

Most libraries not written by Google have some kind of backwards compatibility policy. This is for good reasons. For example, if Debian updates libpng because there's a new RCE, it's ideal if they can update every package to the same new version of libpng all at once. If we go to the extreme of "exact dependencies for every package", then this would actually mean that you have to update every dependent package to a new release that has the new version of libpng, all at the same time, across all supported versions of the distribution. Not to mention, imagine the number of duplicate libraries. Many Linux distros, including Debian, have adopted a policy of only having one version of any given library across the whole repo. As far as I understand, that even includes banning statically linked copies, requiring potentially invasive patching to make sure that downstream packages use the dynamically linked system version. And trust me, if they want to do this, they *will* do this. If they can do it for Chromium, they sure as hell can do it for literally any package.

There's a balance, of course. If a distro does invasive patching and it is problematic, I think most people will be reasonable about it and accept that they need to report the issue to their distribution instead. Distros generally do accept bugs for the packages that they manage, and honestly for most packages, by the time a bug gets to you, there is a pretty reasonable chance that it's actually a valid issue, so throwing away the issue simply because it came from someone running an "unofficial" build seems really counterproductive and definitely not in the spirit of open source.

Reproducibility is good for many reasons. I do not feel it is a good excuse to just throw away potentially valid bug reports though. It's not that maintainers are under any obligation to actually act on bug reports, or for that matter, even accept them at all in the first place, but if you do accept bugs, I think that "this is broken in new version of Clang" is a very good and useful bug report that likely signals a problem.

charcircuit
3 replies
14h37m

For example, if Debian updates libpng because there's a new RCE, it's ideal if they can update every package to the same new version of libpng all at once.

It Debian is upgrading a dependency instead of a developer, then Debian should be ready to fix any bugs they introduce.

then this would actually mean that you have to update every dependent package to a new release that has the new version of libpng, all at the same time, across all supported versions of the distribution

This is already how it works. All vulnerable programs make an update and try to hold off in releasing it until near an embargo date. You don't have to literally update them all at the same time. It's okay of some are updated at different times than others.

Not to mention, imagine the number of duplicate libraries.

Duplicate libraries are not an issue.

Many Linux distros, including Debian, have adopted a policy of only having one version of any given library across the whole repo.

This is a ridiculous policy to me as you are forcing programs to use dependencies they were not designed for. This is something that should be avoided as much as possible.

by the time a bug gets to you, there is a pretty reasonable chance that it's actually a valid issue

That doesn't mean there isn't damage done. There are many people who consider kdenlive an unstable program that constantly crashes because of distros shipping it with the incorrect dependencies. This creates reputational damage.

jchw
2 replies
13h36m

It Debian is upgrading a dependency instead of a developer, then Debian should be ready to fix any bugs they introduce.

That's what the Debian Bug Tracking System is for. However, if the package is actually broken, and it's because e.g. it uses the dependency improperly and broke because the update broke a bad assumption, then it would ideally be reported upstream.

This is already how it works. All vulnerable programs make an update and try to hold off in releasing it until near an embargo date. You don't have to literally update them all at the same time. It's okay of some are updated at different times than others.

That's not how it works in the vast majority of Linux distributions, for many reasons, such as the common rule of having only one version, or the fact that Debian probably does not want to update Blender to a new major version because libpng bumped. That would just turn all supported branches of Debian effectively into a rolling release distro.

Duplicate libraries are not an issue.

In your opinion, anyway. I don't really think that there's one way of thinking about this, but duplicate libraries certainly are an issue, whether you choose to address them or not.

This is a ridiculous policy to me as you are forcing programs to use dependencies they were not designed for. This is something that should be avoided as much as possible.

Honestly, this whole tangent is pointless. Distributions like Debian have been operating like this for like 20+ years. It's dramatically too late to argue about it now, but if you're going to, this is not exactly the strongest argument.

Based on this logic, effectively programs are apparently usually designed for exactly one specific code snapshot in time of each of its dependencies.

So let's say I want to depend on two libraries, and both of them eventually depend on two different but compatible versions of a library, and only one of them can be loaded into the process space. Is this a made-up problem? No, this exact thing happens constantly, for example with libwayland.

Of course you can just pick any newer version of libwayland and it works absolutely perfectly fine, because that's why we have shared libraries and semver to begin with. We solved this problem absolutely eons ago. The solution isn't perfect, but it's not a shocking new thing, it's been the status quo for as long as I've been using Linux!

That doesn't mean there isn't damage done. There are many people who consider kdenlive an unstable program that constantly crashes because of distros shipping it with the incorrect dependencies. This creates reputational damage.

If you want your software to work better on Linux distributions, you could always decide to take supporting them more seriously. If your program is segfaulting because of slightly different library versions, this is a serious problem. Note that Chromium is a vastly larger piece of software than Kdenlive, packaged downstream by many Linux distributions using this very same policy, and yet it is quite stable.

For particularly complex and large programs, at some point it becomes a matter of, OK, it's literally just going to crash sometimes, even if distributions don't package unintended versions of packages, how do we make it better? There are tons of avenues for this, like improving crash recovery, introducing fault isolation, and simply, being more defensive when calling into third party libraries in the first place (e.g. against unexpected output.)

Maintainers, of course, are free to complain about this situation, mark bugs as WONTFIX INVALID, whatever they want really, but it won't fix their problem. If you don't want downstreams, then fine: don't release open source code. If you don't want people to build your software outside of your exact specification because it might damage its reputation, then simply do not release code whose license is literally for the primary purpose of making what Linux distributions do possible. You of course give up access to copyleft code, and that's intended. That's the system working as intended.

I believe that ultimately releasing open source code does indeed not obligate you as a maintainer to do anything at all. You can do all manner of things, foul or otherwise, as you please. However, note that this relationship is mutual. When you release open source code, you relinquish yourself of liability and warranty, but you grant everyone else the right to modify, use and share that code under the terms of the license. Nowhere in the license does it say you can't modify it in specific ways that might damage your program's reputation, or even yours.

charcircuit
1 replies
12h1m

That's what the Debian Bug Tracking System is for.

Software should be extensively tested and code review should be done before it gets shipped to users. Most users don't know about the Debian Bug Tracking system, but they do know about upstream.

Honestly, this whole tangent is pointless. Distributions like Debian have been operating like this for like 20+ years. It's dramatically too late to argue about it now, but if you're going to, this is not exactly the strongest argument.

It's not too late as evidence by the growth of solutions like appimage and flatpak which allows developers to avoid this.

So let's say I want to depend on two libraries, and both of them eventually depend on two different but compatible versions of a library, and only one of them can be loaded into the process space. Is this a made-up problem? No, this exact thing happens constantly, for example with libwayland.

Multiple versions of a library can be loaded into the same address space. Developers can choose to have their libraries support a range of versions.

that's why we have shared libraries and semver to begin with

Hyrum's Law. Semver doesn't prevent breakages on minor bumps.

jchw
0 replies
2h16m

Software should be extensively tested and code review should be done before it gets shipped to users.

That's why distributions have multiple branches. Debian Unstable packages get promoted to Debian Testing, which get promoted to a stable Debian release. Distributions do bug tracking and testing.

Most users don't know about the Debian Bug Tracking system, but they do know about upstream.

There are over 80,000 bugs in the Debian bug tracker. There are over 144,000 bugs in the Ubuntu bug tracker. It would suffice to say that a lot of users indeed know about upstream bug trackers.

I am not blaming anyone who did not know this. It's fully understandable. (And if you ask your users to please go report bugs to their distribution, I think most distributions will absolutely not blame you or get mad at you. I've seen it happen plenty of times.) But just FYI, this is literally one of the main reasons distributions exist in the first place. Most people do not want to be in charge of release engineering for an entire system's worth of packages. All distributions, Debian, Ubuntu, Arch, NixOS, etc. wind up needing THOUSANDS of at least temporarily downstream patches to make a system usable, because the programs and libraries in isolation are not designed for any specific distribution. Like, many of them don't have an exact build environment or runtime environment.

Flatpak solves this, right? Well yes, but actually no. When you target Flatpak, you pick a runtime. You don't get to decide the version of every library in the runtime unless you actually build your own runtime from scratch, which is actually ill-advised in most cases, since it's essentially just making a Linux distribution. And yeah. That's the thing about those Flatpak runtimes. They're effectively, Linux distributions!

So it's nice that Flatpak provides reproducibility, but it's absolutely the same concept as just testing your program on a distro's stable branch. Stable branches pretty much only apply security updates, so while it's not bit-for-bit reproducible, it's not very different in practice; Ubuntu Pro will flat out just default to automatically applying security updates for you, because the risk is essentially nil.

It's not too late as evidence by the growth of solutions like appimage and flatpak which allows developers to avoid this.

That's not what AppImage is for, AppImage is just meant to bring portable binaries to Linux. It is about developers being able to package their application into a single file, and then users being able to use that on whatever distribution they want. Flatpak is the same.

AppImage and Flatpak don't replace Linux distribution packaging, mainly because they literally can not. For one thing, apps still have interdependencies even if you containerize them. For another, neither AppImage nor Flatpak solve the problem of providing the base operating system for which they run under, both are pretty squarely aimed at providing a distribution platform specifically for applications the user would install. The distribution inevitably still has to do a lot of packaging of C and C++ projects no matter what happens.

I do not find AppImage or Flatpak to be bad developments, but they are not in the business of replacing distribution packaging. What it's doing instead is introducing multiple tiers of packaging. However, for now, both distribution methods are limited and not going to be desirable in all cases. A good example is something like OBS plugins. I'm sure Flatpak either has or will provide solutions for plugins, but today, plugins are very awkward for containerized applications.

Multiple versions of a library can be loaded into the same address space. Developers can choose to have their libraries support a range of versions.

Sorry, but this is not necessarily correct. Some libraries can be loaded into the address space multiple times, however, this is not often the case for libraries that are not reentrant. For example, if your library has internal state that maintains a global connection pool, passing handles from one instance of the library to the other will not work. I use libwayland as an example because this is exactly what you do when you want to initialize a graphics context on a Wayland surface!

With static linking, this is complicated too. Your program only has one symbol table. If you try to statically link e.g. multiple versions of SDL, you will quickly find that the two versions will in fact conflict.

Dynamic linking makes it better, right? Well, not easily. We're talking about Linux, so we're talking about ELF platforms. The funny thing about ELF platforms is that the way the linker works, there is a global symbol table and the default behavior you get is that symbols are resolved globally and libraries load in a certain order. This behavior is good in some cases as it is how libpthreads replaces libc functionality to be thread-safe, in addition to implementing the pthreads APIs. However it's bad if you want multiple versions, as instead you will get mostly one version of a library. In some catastrophic cases, like having both GTK+2 and GTK3 in the same address space, it will just crash as you call a GTK+2 symbol that tries to access other symbols and winds up hitting a GTK3 symbol instead of what it expected. You CAN resolve this, but that's the most hilarious part: The only obvious way to fix this, to my knowledge, is to compile your dependencies with different flags, namely -Bsymbolic (iirc), and it may or may not even compile with these settings; they're likely to be unsupported by your dependencies, ironically. (Though maybe they would accept bug reports about it.) The only other way to do this that I am aware of is to replace the shared library calls with dlopen with RTLD_LOCAL. Neither of these options are ideal though, because they require invasive changes: in the former, in your dependencies, in the latter, in your own program. I could be missing something obvious, but this is my understanding!

Hyrum's Law. Semver doesn't prevent breakages on minor bumps.

Hyrum's law describes buggy code that either accidentally or intentionally violates contracts to depend on implementation details. Thankfully, people will, for free, report these bugs to you. It's legitimately a service, because chances are you will have to deal with these problems eventually, and "as soon as possible" is a great time.

Just leaving your dependencies out of date and not testing against newer versions ever will lead to ossification, especially if you continue to build more code on top of other flawed code.

Hyrum's law does not state that it is good that people depend on implementation details. It just states that people will. Also, it's not really true in practice, in the sense that not all implementation details will actually wind up being depended on. It's true in spherical cow land, but taking it to its "theoretical" extreme implies infinite time and infinite users. In the real world, libraries like SDL2 make potentially breaking changes all the time that never break anything. But even when people do experience breakages as a result of a change, sometimes it's good. Sometimes these breakages reveal actual bugs that were causing silent problems before they turned into loud problems. This is especially true for memory issues, but it's even true for other issues. For example, a change to the Go programming language recently fixed a lot of accidental bugs and broke, as far as anyone can tell, absolutely nobody. But it did lead to "breakages" anyways, in the form of code that used to be accidentally not actually doing the work it was intended to do, and now it is, and it turns out that code was broken the whole time. (The specific change is the semantics change to for loop binding, and I believe the example was a test suite that accidentally wasn't running most of the tests.)

Hyrum's law also describes a phenomena that happens to libraries being depended on. For you as a user, you should want to avoid invoking Hyrum's law because it makes your life harder. And of course, statistically, even if you treat it as fact, the odds that your software will break due to an unintended API breakage is relatively low; it's just higher that across an entire distribution's worth of software something will go wrong. But for your libraries, they actually know that this problem exists and do their best to make it hard to rely on things outside the contract. Good C libraries use opaque pointers and carefully constrain the input domain on each of their APIs to try to expose as little unintended API surface area as humanly possible. This is a good thing, because again, Hyrum's law is an undesirable consequence!

pizlonator
0 replies
17h53m

I think this depends on a bunch of stuff.

- Who are the consumers of the source code, i.e. who will ever check it out and build it? Sometimes, it's just one person. Sometimes, it's a team of engineers. In that case, -W -Werror is fine.

- How does a warning being reported make the engineers on the team feel? If the answer is, "Hold my beer for five minutes while I commit a fix", then -W -Werror might be the right call. I've been on projects like that and some of them had nontrivial source code consumers.

- How easy is it to hack the build system? Some projects have wonderfully laid out build systems. If that's the case and -W -Werror is the default, then it's not hard to go in there and change the default, if the -Werror creates problems.

- Does the project have a facility (in the build system) and policy (as a matter of process) to just simply add -Wno-blah-blah as the immediate fix for any new warning that arises? I've seen that, too.

(I'm using -Werror in some parts of a personal project. If you're a solo maintainer of a codebase that can be built that way, then it's worth it - IMO much lower cognitive load to never have non-error warnings. The choice of what to do when the compiler complains is a more straightforward choice.)

galangalalgol
0 replies
20h4m

I would do wall wextra and werror. Again mostly for my own sanity. But I'd wait to add werror until they were all fixed so regression testing would continue as the warnings got fixed. Cpp_check and clang tidy would also eventually halt the pipeline. And *san on the tests as compiled in both debug and O3 with a couple compilers.

__d
3 replies
11h15m

That's a feature!

New warnings added to new compiler versions can identify problems that weren't previously detected. You _want_ those to -Werror when they happen, so you can fix them if they need it.

Changing a compiler version is a task that has to be resourced appropriately. Part of that is dealing with any fallout like this. Randomly updating your compiler is just asking for trouble.

adastra22
2 replies
9h57m

It is certainly not a feature because it make all infrastructure including just regular old checkout-and-build workflows break for historical versions of the code. It’s so annoying to have to checkout an older version and then have to go disable -Wall -Werror everywhere just to get the damn thing to build.

Keep master clean of any warnings, for sure. But don’t put it straight into the build system defaults.

ordu
0 replies
6h29m

Just updating a compiler could break workflows for historical versions of the code. It is unavoidable. But it is easier with build flags if you use VCS: these flags could be different for different versions.

__float
0 replies
3h7m

If you store the compiler version in source control, then you don't have this problem.

planede
1 replies
8h24m

IMO there is absolutely no reason to enable warnings in CI without -Werror. Nobody reads the logs of a successful build.

If some warnings are flaky then disable them specifically. In my experience most warnings in -Wall are OK and you can suppress the rare false positives in code. Don't suppress without a comment.

edit:

Having said that there are entirely valid reasons to not have -Werror outside of CI. It should be absolutely disabled by default if you distribute source.

mst
0 replies
2h32m

This is the sort of situation where I'll consider progressive testing initially - i.e. write out the existing warnings to a file that you commit, add a test that fails if you get any that aren't in the file.

As you fix the inherited ones you can regenerate the file, hopefully smaller each time.

"If I don't have time to fix all of this -now-, I can at least make sure it can't get any -worse- in the mean time" is a very useful approach when automating the 'make sure' is something quick enough that you can find time for that.

nanolith
0 replies
9h10m

The compiler and toolchain is a dependency like any other. Upgrading to a new compiler version is an engineering task just like upgrading a library version. It must be managed as such. If this leads to new errors, then this becomes part of the upgrade management. Likewise, since the code generator and optimizers have changed, this upgrade must be tested like any other new feature or fix. Create an appropriate topic/feature branch, and dig in.

bluGill
0 replies
3h20m

-Wall and -Werror should be running on all developer machines and your CI machines.

If you are delivering source code to someone else (or getting source code from someone else that you build but do not otherwise work on then you should ensure warnings are disabled for those builds. However all developers and your CI system still needs to run with all warnings possible.

The days when compilers put in warnings that were of questionable value are mostly gone. Today if you see a warning from your compiler it is almost always right.

hobofan
5 replies
8h43m

wrap your build environment with docker (or your favorite packager) so that your tooling and dependencies become both explicit and reproducable

If you want explicitness and reproducibility please don't reach for Docker. Unless you take a lot of care, you will only get the most watered down version of reproducibility with Docker probably luring you into a false sense of security. E.g. pointing to mutable image tags without integrity hashes and invoking apt-get are things you'll find in most Dockerfiles out there and both leave open a huge surface area for things to go wrong and end up in slightly different states.

And while they are not that easy to pick up, solutions like Bazel and Nix will give you a lot better foundation to stand on.

hawk_
1 replies
4h1m

If you want explicitness and reproducibility please don't reach for Docker

Is it common in C++ builds to rely on the current O/S libraries instead of say making most dependencies explicit, close to full cross-compile? Do dependencies need to be pulled in using apt-get and not something like maven?

SJC_Hacker
0 replies
3h18m

If you care about security and bug fixes, then yes

From what I've seen the "minor version" is fixed. e.g. FROM ubuntu:22.04 and not FROM ubuntu:laatest

bluGill
0 replies
3h52m

I have docker as part of my reproducibility builds. We recently ran into a problem trying to rebuild some old code - turns out the ssl certificates in our older images has expired and so now the code isn't reproducible anyway. One more reason to agree with the idea that docker shouldn't be used.

Though we use docker for a different reason: we are building on linux, targeting linux. It is really easy to use the host system headers or library instead of the target versions of the same - and 99% of the time you will get away with it as they are the same. Then several years later you upgrade your host linux machine and can't figure out why a build that used to work isn't (and since 99% of the stuff is the same this can be months before you test that one exception and then it crashes). Docker ensures we don't install any host headers, or libraries except what is needed for the compiler.

basicallybones
0 replies
5h4m

A nice middle ground is using a tool like Google's Skaffold, which provides "Bazel-like" capabilities for composing Docker images and tagging them based on a number of strategies, including file manifests. In my case, I also use build args to explicitly set versions of external dependencies. I also pull external images and build base images with upgraded versions once, then re-tag them in my private repository, which is an easy-to-implement mechanism for reproducibility.

While I am in a Typescript environment with this setup at the moment, my personal experience that Skaffold with Docker has a lighter implementation and maintenance overhead than Bazel. (You also get the added benefit of easy deployment and automatic rebuilds.)

I quite liked using Bazel in a small Golang monorepo, but I ran into pain when trying to do things like include third-party pre-compiled binaries in the Docker builds, because of the unusual build rules convention. The advantage of Skaffold is it provides a thin build/tag/deploy/verify layer over Docker and other container types. Might be worth a look!

Kudos to the Google team building it! https://skaffold.dev

SJC_Hacker
0 replies
3h41m

If you want explicitness and reproducibility please don't reach for Docker. Unless you take a lot of care, you will only get the most watered down version of reproducibility with Docker probably luring you into a false sense of security. E.g. pointing to mutable image tags without integrity hashes and invoking apt-get are things you'll find in most Dockerfiles out there and both leave open a huge surface area for things to go wrong and end up in slightly different states.

If this is frequently a problem you're doing something wrong, or using such a crappy external library/toolchain that breaks frequently on the same version.

Docker is a way to ensure that the software builds with "the most recent minor version" of some OS/toolchain/libraries.

The reason why you want the most recent version is because of security fixes and bugs.

I agree that you should check integrtiy hashes where appropriate, if you really want to fix versions.

dheera
2 replies
15h15m

wrap your build environment with docker

For microservices it is fine, but you can't always deploy everything else with docker, especially for people who want to use your app inside a docker. Docker-in-docker is a situation that should never happen.

Containers are nice but they're a horrible way to pretend the problem doesn't exist.

Bundle all the dependencies and make sure it doesn't depend on 5 billion things being in /usr/lib and having the correct versions.

cshokie
1 replies
13h27m

Not the OP, but I don’t think they meant that the build output is in a container. They meant that the thing you use to compile the code is in docker (and you just copy out the result). That would help ensure consistency of builds without having any effect on downstream users.

eschneider
0 replies
3h19m

Exactly. The compiler and what ever dependencies you need _to build_ are bundled into a docker so that you don't need to worry about whatever random tools/libraries your coworkers have installed in their local environment.

golergka
0 replies
19h52m

Great advice. Almost all of it applies to any programming language.

SleepyMyroslav
0 replies
21h23m

I would not call that 'controversial'. In the internet days people call this behavior trolling for a reason. The punchline about rewriting code in different language gives an easy hint at where this all going.

PS. I have been in the shoes of inheriting old projects before. And I hope i left them in better state than they were before.

keepamovin
33 replies
1d2h

It's funny. My first step would be

  0. You reach out to the previous maintainers, visit them, buy them tea/beer and chat (eventually) about the codebase. Learned Wizards will teach you much.
But I didn't see that anywhere. I think the rest of the suggestions (like get it running across platform, get tests passing) are useful stress tests likely to lead you to robustness and understanding however.

But I'd def be going for that sweeet, sweet low-hangin' fruit of just talking to the ol' folks who came that way before. Haha :)

Night_Thastus
14 replies
1d2h

IME, this only works if you can get regular help from them. A one-off won't help much at all.

mellutussa
7 replies
1d1h

A one-off won't help much at all.

Monumentally disagree. One-off session with a guy who knows the codebase inside out can save you days of research work. Plus telling you all about the problematic/historical areas.

Night_Thastus
5 replies
1d1h

I'm just stating my experience. A single day, if they still have access to the codebase might be able to clear up some top-level concepts.

But the devil is in all the tiny details. What is this tiny correction factor that was added 20 years ago? Why was this value cut off to X decimals? Why didn't they just do Y here? Why do we override this default behavior?

It's tens of thousands of tiny questions like that which you can't ask until you're there.

slingnow
4 replies
20h23m

I don't understand what you're saying. Clearly both types of meetings (one-off vs recurring) would be helpful. The one-off may save you days/weeks of research, but it seems like you're not satisfied with that unless you can answer every single minor question you might have across the entire codebase.

Night_Thastus
3 replies
14h21m

I'm saying that if you're maintaining a code base for years, a single day's explanations won't do much of anything. It's a drop in the bucket.

It's not a bad thing, and it's certainly good to do, but it's not a solution to the problem.

mellutussa
2 replies
10h56m

If your granularity for a task is measured in years then you have a much different and harder problem.

Effectively everything becomes a "drop in the bucket".

rightbyte
1 replies
3h33m

Having a one day intro might save you two weeks of the 6 - 36 month task of getting to know the code base.

Like ... it doesn't help that much. Mainly it saves some frustration of the build system is insane or the source code is spread out in mails, a hd and a floppy in a drawer or the current source state is broken and need to be reverted.

But if the original author is there to do a handover, the chances are the company is properly run and the work will be a breeze anyway becouse the code is good and well structured.

mellutussa
0 replies
2h21m

We'll just have to accept that we live in different worlds. I still consider what you described a massive save.

keepamovin
0 replies
12h52m

Exactly. Someone to guide you in the right path. Gonna help a lot! Hahaha :)

Joel_Mckay
3 replies
1d1h

I've always found discussing why former employees left a project incredibly enlightening. They will usually explain the reality behind the PR given they are no longer involved in the politics. Most importantly they will often tell you your best case future with a firm.

Normally, employment agreements specifically restrict contact with former staff, or discussions of sensitive matters like compensation packages.

C++ is like any other language, in that it will often take 3 times longer to understand something than re-implement the same. If you are lucky, than everything is a minimal API lib, and you get repetitive examples of the use cases... but the cooperative OSS breadcrumb model almost never happens in commercial shops...

Legacy code bases can be hell to work with, as you end up with the responsibility for 14 years of IT kludges. Also, the opinions from entrenched lamers on what productivity means will be painful at first.

Usually, with C++ it can become its own project specific language variant (STL or Boost may help wrangle the chaos).

You have my sympathy, but no checklist can help with naive design inertia. Have a wonderful day. =)

saagarjha
1 replies
8h59m

Note that depending on your jurisdiction discussion of compensation may be a right protected by law.

Joel_Mckay
0 replies
7h36m

Contract law has weird consequences in different places.

Indeed, if the legal encumbrance is not legal, than its often unenforceable.

Talking with your own lawyers before doing something silly is a good habit. ;-)

keepamovin
0 replies
12h51m

Uh, yeah, that's a great idea, too! That's the big question. What happened to the previous team? Can give you org insights as well as code ones. Info on company strategy, priorities, work cadence. Actually a pretty good open ended conversation opener! Hahaha! :)

keepamovin
1 replies
1d1h

Yeah you need to cultivate those relationships. But with a willing partner that first session will take you from 0 to 1 :)

mst
0 replies
2h10m

A lot of the time it takes you from 0 to 0.1, but (a) every little helps (b) if you take them out for lunch or a post-work beer or whatever it can build a relationship where you can ask follow up questions via email.

Ideal is if they have a life such that something morally equivalent to "how about we meet up and I'll pay for the food/beer" is a viable thing to suggest later.

Oh, and always remember - the way to a geek's schedule is often through their wife. If you get a chance to meet their partner, TAKE IT and be on your best behaviour, if $partner likes you then you have massively increased chances of making useful things happen later.

raverbashing
4 replies
1d2h

Easier said than done

My step 0 would be: run it through an UML tool to get a class diagram and other diagrams.

This will help you a lot.

Get the tests passing on your machine

Tests? On a C++ codebase? I like your optimism, rs

Joel_Mckay
3 replies
1d1h

No one has time to maintain the UML in production.

You may luck out with auto-generated documentation, but few use these tools properly (javadoc or doxygen). =)

cratermoon
2 replies
1d1h

The GP said nothing about keeping and maintaining it, only generating it. Use it to understand the codebase, then archive it or throw it out.

Jtsummers
1 replies
1d1h

Exactly. You inherited 500k SLOC of C++ that grew together since 1985. You don't know the interconnections between the classes that have accumulated in that time. It was also developed by multiple teams, and likely had very different approaches to OO during these past nearly 40 years. The UML diagrams won't tell you everything, but they will tell you things like the inheritance hierarchy (if this was a 1990s C++ project it's probably pretty nasty), what classes are referenced by others via member variables, etc. This can be hugely informative when you want to transform a program into something saner.

Joel_Mckay
0 replies
1d

I always interpreted most polymorphism as sloppy context-specific state-machine embedding, and fundamentally an unmaintainable abomination from OOP paradigms.

OO requires a lot of planning to get right (again, no shop will give your team time to do this properly), and in practice it usually degenerates into spiral development rather quickly (<2 years). Thus, 14 years later what you think you see in documentation may be completely different from the conditional recursive definition some clown left for your team (yes, it happens eventually...)

500k lines is not that bad if most of it is encapsulated libraries... =)

theamk
2 replies
1d

Maybe do a quick look at codebase first so you can identify biggest WTF's and ask about them.

After all, if you have inherited a codebase with no tests, with build process which fails every other time, with unknown dependency info, and which can only be built on a single server with severely outdated OS... are you sure the previous maintainer is a real wizard and all the problems are result of not enough time? Or are they a "wizard" who keep things broken for job security and/or because they don't want to learn new things?

vaylian
0 replies
9h35m

Even if that person is not a great archwizard, they still have more experience in that project than you and you will probably learn some things that will make your life less miserable, because you will better understand what to expect and what kind of failed solutions have been tried before.

keepamovin
0 replies
12h53m

Yes! Good idea. Locking in at -1. Hahah! :)

TrackerFF
2 replies
5h44m

I was once tasked with deploying a piece of software on a closed network (military), to run on a old custom OS - it wasn't a huge program, around 50k lines of code.

I did encounter a bunch of bugs and problems underway, and wanted to reach out to the devs that wrote it - as it was customer made for my employer.

Welp, turns out it was written by one guy/contractor, and that he had passed away a couple of years earlier.

At least in the defense industry you'll find these sort of things all the time. Lots of custom/one-off stuff, made for very specific systems. Especially on the hardware side it is not uncommon that the engineers that made the equipment are either long gone or retired.

keepamovin
1 replies
4h53m

Welp, you might have needed to consult a seance for that one.

Interesting point about the "author expiry" window. I was thinking about that today regarding something else:

Let's say in 20 years time, no database code has been updated for the last 20 years. And all the people who worked on it, can't remember anything about it. Yet, it still works.

That means that everyone who uses that database everyday, doesn't know how it works. They believe that it works, this belief is widespread. And the database providing results to queries, is a real thing. And it does work -- but nobody knows how.

This is common. I don't know in any detail how the MacBook I use works. But it does. I don't know how many things I use actually work. But they do work.

It seems the only difference, in the world of things that "work", and which most people who use them do not understand how they work, is that there are two classes of things: those things for which there is a widespread belief that they do work; and those things for which the belief that they work, is not widespread. But in either case, they work.

TrackerFF
0 replies
4h0m

Even more common in the world of industrial automation.

Lots of old early-gen PLCs from the 70s/80s still ticking, with no documentation and the techs/engineers/companies long gone.

We worked on one such PLC, around 3 decades old at the point, and it came down to probing I/O, reverse engineering the functionality.

But at some point, if there hasn't been enough legacy support, there comes a time where people just have to bite the bullet and re-build a system from the ground up - and integrate it in parallel with the old system running, until it can be removed completely.

Too bad many of the old and forgotten systems are still running and integral, so they get put inside a glass cage with "DON'T TOUCH!" warning sticker.

GuB-42
2 replies
1d1h

I wouldn't make it the first step. If you do, you will probably waste their time more than anything.

Try to work on it a little bit first, and once you get stuck in various places, now you can talk to the previous maintainers, it will be much more productive. They will also appreciate the effort.

keepamovin
0 replies
12h54m

Yeah, that's fair enough. I guess since we're already zero-indexed maybe my -1 step is Prep. Hahaha! :)

guhidalg
0 replies
19h37m

There’s a fine balance with no right or wrong answer. Previous maintainers will appreciate if you spent literally more than a second trying to understand before you reach out to them, but for your own sanity you should know when it’s time to stop and call for help.

lelanthran
1 replies
11h4m

It's funny. My first step would be

0. You reach out to the previous maintainers, visit them, buy them tea/beer and chat (eventually) about the codebase. Learned Wizards will teach you much.

Have you ever tried that? This is legacy code. Even if the handover was yesterday, they cannot tell you about anything useful they did more than 6m in the past.

And that's the best-case scenario. The common-case is "That's the way I got it when it was handed over to me" answer to every question you ask.

mst
0 replies
2h15m

I have, and assuming your predecessor doesn't mind, you still get enough useful answers to make it worth it a lot of the time.

Especially if you manage to get to just chatting about the project - at some point they'll almost certainly go "oh! while I remember," followed by the answer to a question you didn't realise you should've asked.

The value is often in the commiseration rather than the interrogation, basically.

planede
0 replies
8h11m

You mean the guys the company laid off last week?

fransje26
0 replies
8h35m

    > You reach out to the previous maintainers, visit them
I could have brought them flowers, and shared a moment of silence contemplating eternity. I don't know if it would significantly have helped understanding the code base though..

Jtsummers
30 replies
1d1h

I'd swap 2 and 3. Getting CI, linting, auto-formatting, etc. going is a higher priority than tearing things out. Why? Because you don't know what to tear out yet or even the consequence of tearing them out. Linting (and other static analysis tools) also give you a lot of insight into where the program needs work.

Things that get flagged by a static analysis tool (today) will often be areas where you can tear out entire functions and maybe even classes and files because they'll be a re-creation of STL concepts. Like homegrown iterator libraries (with subtle problems) that can be replaced with the STL algorithms library, or homegrown smart pointers that can just be replaced with actual smart pointers, or replacing the C string functions with C++'s on string class (and related classes) and functions/methods.

But you won't see that as easily until you start scanning the code. And you won't be able to evaluate the consequences until you push towards more rapid test builds (at least) if not deployment.

dralley
21 replies
1d1h

On the flip side, auto-formatting will trash your version history and impede analysis of "when and why was this line added".

Jtsummers
11 replies
1d1h

I'm not hardcore on auto-formatters, but I think their impact on code history is negligible in the case of every legacy system I've worked on. The code history just isn't there. These aren't projects that used git until recently (if at all). Before that they used something else, but when they transitioned they didn't preserve the history. And that's if they used any version control system. I've tried to help teams whose idea of version control was emailing someone (they termed them "QA/CM") to make a read-only backup of the source directory every few months (usually at a critical review period in the project, so a lot of code was changed between these snapshots).

That said, sure, skip them if you're worried about the history getting messed up or use them more selectively.

KerrAvon
7 replies
22h26m

SVN was a thing by the mid-2000's, and history from that is easy to preserve in git. Just how old are the sourcebases in question? (Not to shoot the messenger; just like, wow.)

edit:typo

varjag
1 replies
22h22m

The first large C++ project I worked on in mid-1990s was basically preserving a bunch of archived copies of the source tree. CVS was a thing but not on Windows, and SourceSafe was creating more problems than it been solving.

mst
0 replies
2h25m

I kept regular tarballs of a project that used SourceSafe right near the start of my career, and found I was more likely to be able to find an intact copy of the right thing to diff against from my tarballs.

I think after a year or so I realised that even bothering to -try- to use SourceSafe was largely silly, got permission to stop, and installed a CVS server on a dev box for my own use.

(yes I know the VCS server shouldn't really be on the dev box I could potentially trash, I didn't have another machine handy and it was still a vast improvement)

pyuser583
0 replies
12h8m

I've had issues doing decent copies from SVN to GIT. They both have different ideas about user identity, and how fragmented it can be.

olvy0
0 replies
10h38m

I maintain a C++ codebase that was originally written in 1996, and is mission critical for my organization. Originally maintained in Visual Sourcesafe, then in TFS source control, and now git. Some parts of it were rewritten (several times) in C#, but the core is still C++.

I was very worried when we transitioned to git that history will not be preserved and tried to preserve it, but it proved too much hassle so I dropped it.

In fact that proved not to be a problem. Well, not a problem for me, since I remember all the history of the code and all the half forgotten half baked features and why they are there. But if I'm gone then yes, it's going to be a problem. It's in a dire need for a rewrite, but this has been postponed again and again.

jamesfinlayson
0 replies
16h45m

I looked at a C++ codebase from 1997 at a previous job - I don't know much about the history but comments in one of the old files tracked dates and changes to 2001. Not sure what happened after that but in 2017 someone copy-pasted the project from TFS to git and obliterated the prior history.

Pfiffer
0 replies
22h5m

I've heard a lot of stories about mid-90s codebases for sure

Jtsummers
0 replies
21h58m

Some of these systems dated back to the 1970s. The worst offenders were from the 1980s and 1990s though.

It's all about the team or organization and their laziness or non-laziness.

bear8642
2 replies
19h53m

I think their impact on code history is negligible in the case of every legacy system I've worked on. The code history just isn't there.

Not sure if I agree here or not - whilst yes, the history isn't there, if it's a small enough team you'll have a good guess at who wrote it.

Definitely found I've learnt the style of colleages so know who to ask just from the code outline.

Jtsummers
1 replies
19h43m

Legacy systems that you inherit don't have people coming with them very often. That's part of the context of this. You often don't have people to trace it back to or at least not the people who actually wrote it (maybe someone who worked with them before they got laid off a decade ago), and reformatting the code is not going to make it any harder to get answers from people who aren't there.

mst
0 replies
2h28m

I've been in situations where even without access to the people knowing which of them wrote something gives me a better idea of how to backwards infer what (and of course sadly occasionally 'if') they were thinking while writing the code.

Then again, I think most of the tells for that for me are around the sort of structure that would survive reformatting anyway.

(and, y'know, legacy stuff, everything's a bloody trade-off)

skrebbel
1 replies
1d1h

You can ignore commits from git blame by adding them to a .gitattributes file.

This is assuming Git of course, which is not a given at all for the average legacy c++ codebase.

fransje26
0 replies
8h56m

Good to know. Thanks for the tip!

lpapez
1 replies
20h55m

You can instruct git to ignore specific commits for blame and diff commands.

See "git blame ignore revs file".

Intended use is exactly to ignore bulk changes like auto formatting.

mb7733
0 replies
17h36m

How does reformatting trash the history? It's one extra commit..

I guess if it splits or combines lines that could cause some noise if you really want the history of a single line... But that happens all the time, and I don't see how it would really prevent understanding the history. You can always do a blame on a range of lines.

Maybe I'm missing something though, genuinely curious for a concrete example where reformatting makes it hard to understand history!

exDM69
0 replies
22h12m

clang-format can be applied to new changes only, for this very reason.

Adding it will remove white space nitpicking from code review, even if it isn't perfect.

duped
0 replies
19h49m

This is another reason why you should track important information in comments alongside the code instead of trusting VCS to preserve it in logs/commit messages, and to reject weird code missing comments from being merged.

Not saying that fixes decades of cruft because you shouldn't change files without good reason and non-white space formatting is not a good reason, but I'm mentioning it because I've seen people naively belief bullshit like "code is self explanatory" and "the reason is in the commit message"

Just comment your code folks, this becomes less of a problem

PreachSoup
0 replies
23h29m

On per file level it's just 1 commit. It's not really a big deal

IshKebab
0 replies
21h17m

I believe you can configure `git blame` to skip a specific commit. But in my experience it doesn't matter anyway for two reasons:

1. You're going to reformat it eventually anyway. You're just delaying things. The best time to plant a tree, etc.

2. If it's an old codebase and you're trying to understand some bit of code you're almost always going to have to walk through about 5 commits to get to the original one anyway. One extra formatting commit doesn't really make any difference.

politician
2 replies
1d1h

Nit: The post scopes "tearing things out" to dead code as guided by compiler warnings and unsupported architectures.

If going the route, I'd recommend commenting out the lines rather than removing them outright to simplify the diffs at least until you're ready to squash and merge the branch.

SAI_Peregrinus
1 replies
23h12m

Better to use `#if` or `#ifdef` to prevent compilation. C & C++ don't support nested comments, so you can end up with existing comments in the code ending the comment block.

kccqzy
0 replies
21h25m

I think `#if` and `#ifdef` are not good ideas because they prevent the compiler from seeing them in the first place. A better solution is just `if (false)` which is nestable, and the code is still checked by the compiler so it won't bit rot.

thrwyexecbrain
1 replies
20h41m

I would absolutely not recommend auto-formatting a legacy codebase. In my experience large C++ projects tend to have not only code generation scripts (python/perl/whatever) but also scripts that parse the code (usually to gather data for code generation). Auto formatting might break that. I have even seen some really cursed projects where the _users_ parsed public header files with rather fragile scripts.

Jtsummers
0 replies
20h26m

I was listing the items in the original article's #3 and saying I'd move them up to #2 before I'd go about excising portions of the project, the original #2. I still stand by that. But you can read my other comment where I don't really defend auto-formatting to see that I don't care either way. I made it about four hours ago so maybe you missed it if you didn't refresh the page in the last few hours.

jasonwatkinspdx
0 replies
21h17m

Yeah, I've done a fair bit of agency work dropping in to rescue code bases, and the first thing I do is run unit tests and check coverage. I add basic smoke tests anywhere they're missing. This actually speeds me up, rather than slowing me down, because once I have reasonably good coverage I can move dramatically faster when refactoring. It's a small investment that pays off.

btown
0 replies
1d1h

CI is different from the others, here! At minimum, building a "happy path(s)" test harness that can run with replicable results, and will run on every one of your commits, is a first step, and also helps to understand the codebase.

And you're jumping around - and you'll have to! - odds are you'll have a bunch of things changed locally, and might accidentally create a commit that doesn't separate out one concern from another. CI will be a godsend at that point.

broken_broken_
0 replies
1d1h

Fair point!

Night_Thastus
22 replies
1d1h

worry not, by adding std::cmake to the standard library and you’ll see how it’s absolutely a game changer

I'm pretty sure my stomach did somersaults on that.

But as for the advice:

Get out the chainsaw and rip out everything that’s not absolutely required to provide the features your company/open source project is advertising and selling

I hear you, but this is incredibly dangerous. Might as well take that chainsaw to yourself if you want to try this.

It's dangerous for multiple reasons. Mainly it's a case of Chesterton's fence. Unless you fully understand why X was in the software and fully understand how the current version of the software is used, you cannot remove it. A worst case scenario would be that maybe a month or so later you make a release and the users find out an important feature is subtly broken. You'll spend days trying to track down exactly how it broke.

Make the project enter the 21st century by adding CI, linters, fuzzing, auto-formatting, etc

It's a nice idea, but it's hard to do. One person is using VIM, another is using emacs, another is using QTCreator, another primarily edits in VSCode.. Trying to get everyone on the same page about all this is very, very hard.

If it's an optional step that requires that they install something new (like commit hook) it's just not going to happen.

Linters also won't do you any good when you open the project and 2000+ warnings appear.

zer00eyz
9 replies
1d1h

> It's a nice idea, but it's hard to do. One person is using VIM, another is using emacs, another is using QTCreator, another primarily edits in VSCode.. Trying to get everyone on the same page about all this is very, very hard.

This is what's wrong with our industry, and it's no longer an acceptable answer. We're supposed to be fucking professional, and if a job needs to build a tool chain from the IDE up we need to learn to use it and live with it.

Built on my machine, with my IDE, the way I like it and it works is not software. It's arts and fucking crafts.

cratermoon
3 replies
23h11m

If you're saying everyone should agree on the same IDE and personal development toolset, I disagree, sort of.

The GP was suggesting the effort to add CI, linters, fuzzing, auto-formatting, etc was too hard. If that can be abandoned entirely, perhaps the legacy codebase isn't providing enough value, and the effort to maintain it would be better spent replacing it. But the implication is that the value outweighs the costs.

Put all the linters, fuzzing, and format checking in an automated build toolchain. Allow individuals to work how they want, except they can't break the build. Usually this will reign in the edge cases using inadequate tools. The "built on my machine, with my IDE, the way I like it and it works" is no longer the arbiter of correct, but neither does the organization have to deal with the endless yak shaving over brace style and tool choice.

eropple
2 replies
20h16m

> neither does the organization have to deal with the endless yak shaving over brace style and tool choice

I hear you, but an organization that fears this, instead of Just Pick Something And Deal With It, is an organization that probably doesn't have the right people in it to succeed at any task more arduous than that.

cratermoon
1 replies
12h31m

Conversely, and organization that imposes arbitrary choices and isn't capable of allowing people do use the tools they know best probably doesn't attract the best people. There are many different kinds of hammers, and making everyone who uses hammers use the same kind is, to say the least, counter productive.

eropple
0 replies
2h31m

I get where you're coming from, but frankly: nah. If you are so in-the-rut that you can't switch, say, text editors or IDEs and are compelled to have an Incredibly Normal Day about it, you're probably not actually possessed of the plasticity to be somebody I want to work with.

I use vim, IntelliJ, Visual Studio, and VSCode at least once every two weeks apiece, and it's no skin off my back to switch. Do thou likewise.

otabdeveloper4
2 replies
9h52m

every single carpenter in the world should use the exact same make and model of saw, for, uh, professionalism reasons
zer00eyz
0 replies
4h7m

Picture the crew that shows up to stick frame your house.

First guy: hand saw and impact driver... cut and screw Second guy: Power Saw, and hammer. Cut and nail. Third guy: Safety glasses and a Nail gun. Forth guy shows up: compressor, asks where the power is (none) and if he can use some tools.

It would not work. You dont build a CNC production line for parts with every CNC being unique. We dont let devs pick what their production server OS is, we dont let them choose random languages. Tooling matters.

noitpmeder
0 replies
7h52m

One person's saw literally triple checks the measurements before cutting, minimizes wastage, runs 3x faster, and is built by a company specializing in making saws.

The other saw was hand forged in a basement by the user, breaks every other day, and has a totally different blade width, and can only be used by the owner.

saagarjha
1 replies
9h12m

Software is arts and crafts :)

zer00eyz
0 replies
13m

It should be less popsicle sticks and paste and more "Arts and Crafts Movement".

For those that dont know, Arts and Crafts movement in the states is known for some pretty interesting pottery that was produced at scale.

https://excellentjourney.net/2015/03/04/art-fear-the-ceramic...

electroly
4 replies
1d1h

It's a nice idea, but it's hard to do. One person is using VIM...

The things the author listed there are commonly not IDE integrated. I've never seen a C++ development environment where cpplint/clang-tidy and fuzzers are IDE integrated, they're too slow to run automatically on keystrokes. Auto-formatting is the only one that is sometimes integrated. All of this stuff you can do from the command line without caring about each user's chosen development environment. You should definitely at least try rather than giving up before you start just because you have two different text editors in use. This is C++; if your team won't install any tools, you're gonna have a bad time. Consider containerizing the tools so it's easier.

throwaway2037
1 replies
17h12m

I've never seen a C++ development environment where cpplint/clang-tidy and fuzzers are IDE integrated

CLion from JetBrains has clang-tidy integrated (real-time).

planede
0 replies
8h15m

I assume it's clangd? It can be used from vim, vscode, ... etc as well and get a uniform IDE diagnostic experience across text editors.

gpderetta
0 replies
19h13m

Clangd will happily run clang-tidy as part of completion/compile-as-you-type/refactor/auto independent.

On the editor/IDE of your choose.

I wouldn't call it fast, but it is quite usable.

chlorion
0 replies
46m

In Emacs I have clangd and clang-tidy running on each key stroke!

The project size is probably a lot smaller than what most people are working on though, and I have a fast CPU and NVME disk, but it's definitely possible to do!

I'm not sure about the fuzzer part though.

j-krieger
2 replies
20h40m

It's a nice idea, but it's hard to do. One person is using VIM, another is using emacs, another is using QTCreator, another primarily edits in VSCode.. Trying to get everyone on the same page about all this is very, very hard.

I must have missed the memo where I could just say no to basic things my boss requires of me. You know, the guy that pays my salary.

saagarjha
1 replies
9h13m

As others have mentioned, none of these things actually change your development workflow. But if they did, you do have the ability to say no. If your boss fails to understand that you have an environment that you're productive in, that sounds like a bad place to work.

flykespice
0 replies
3h38m

Every company have their own workflow adapted to their tooling so that teams can work among themselves frictionless.

It's ok if you use your own tooling you are comfortable with, but you should adapt to their workflow, and the employer has no obligation to tweak their workflow to integrate your own, it's yours to adapt.

theamk
0 replies
13h54m

It's dangerous for multiple reasons. Mainly it's a case of Chesterton's fence. Unless you fully understand why X was in the software...

If this is a function that no one links to, and your project does not mess with manual dynamic linking (or the function is not exposed), then it's pretty safe to remove it. If it's internal utility which does not get packaged into final release package, it is likely be safe to remove too. If it's a program which does not compile because it requires Solaris STREAMS and your targets are Linux + MacOS - kill it with fire.

(Of course removing function calls, or removing functionality that in-use code depends on, is dangerous. But there is plenty of stuff which has no connection to main code)

aaronbrethorst
0 replies
1d1h

An optional step locally like pre-commit hooks should be backed up by a required step in the CI. In other words: the ability to run tests locally, lint, fuzz, format, verify Yaml format, check for missing EOF new lines, etc, should exist to help a developer prevent a CI failure before they push.

As far as linters causing thousands of warnings to appear on opening the project, the developer adding the linter should make sure that the linter returns no warnings before they merge that change. This can be accomplished by disabling the linter for some warnings, some files, making some fixes, or some combination of the above.

Kamq
0 replies
18h34m

It's a nice idea, but it's hard to do. One person is using VIM, another is using emacs, another is using QTCreator, another primarily edits in VSCode.. Trying to get everyone on the same page about all this is very, very hard.

Bullshit, all of these (and additionally C lion) are fairly easy to configure these for, with the possible exception of QTCreator (not a ton of experience on my end).

Just make it a CI requirement, and let everyone figure it out for their own tools. If you can't figure that out, you get to run it as a shell script before you do your PRs. If you can't figure that out, you probably shouldn't be on a legacy C++ project.

IshKebab
0 replies
21h11m

One person is using VIM, ...

I don't get your point. You know you can autoformat outside editors right? Just configure pre-commit and run it in CI. It's trivial.

If it's an optional step that requires that they install something new (like commit hook) it's just not going to happen.

It will because if they don't then their PRs will fail CI.

This is really basic stuff, but knowledge of how to do CI and infra right does seem to vary massively.

bun_terminator
18 replies
1d1h

Rewrite in a memory safe language?

like c++11 and later?

z_open
15 replies
1d1h

How is that memory safe? Even vector out of bounds index is not memory safe.

bun_terminator
10 replies
1d1h

You can access a vector with a function that throws an exception if you so desire

TwentyPosts
9 replies
1d1h

You can also just write no code at all if you so desire. It certainly won't cause any memory issues that way. (Hint: What you yourself decide to write or refrain from writing is not the problem. You're not they only person who ever worked on this legacy codebase, and you want guarantees but default, not having to check every line of code in the entire project.)

bun_terminator
8 replies
23h46m

no you just have to write a githook with some static analysis, like literally everyone who does proper c++. Safety hasn't been an issue in c++ for more than a decade. It's just a made up thing by people who don't use the language but only want to hate.

z_open
7 replies
22h22m

Go look at the CVEs and github issues of modern C++ codebases. Your statement is nonsense. Chromium is still plagued by use after free. How high do you set the bar? Which codebases are we allowed to look at?

bun_terminator
6 replies
22h20m

I had that exact discussion with someone else a while ago. And when you actually go through the chromium memory bugs, it's 100% avoidable with an absolute baseline of competence and not using ancient bugs. It's unfair that C++ always has to compete in its state from 1990s against languages in their current iteration.

z_open
5 replies
22h6m

That's why I asked what the bar was? If Google is writing shitty C++ even with the world's eyes on their code base, who is doing it right? No one writing anything sufficiently complicated that's for sure.

bun_terminator
4 replies
21h20m

However you feel about this issue: It's pretty widely known that google is bad at c++. Most codebases will be of significantly higher quality.

hairyplanner
2 replies
20h38m

Google chrome must be one of the most used (in terms of CPU time) C++ software in the world right now. That means it's been fuzz tested (by the developers as well as by the users and also the random websites that gives it garbage html and javascript) extensively. I can only think of the Linux kernel that is more widely used, and Linux is not C++.

Since you seem to be very good at c++, can you point to a "significantly higher quality" c++ projects please? I'd like to see what it looks like.

delta_p_delta_x
0 replies
19h54m

Since you seem to be very good at c++, can you point to a "significantly higher quality" c++ projects please?

Not the parent commenter, but there are quite a few very high-quality C++ projects out there.

- LLVM

- KDE projects

- CMake

- Node.js

- OpenJDK

bun_terminator
0 replies
13h26m

I don't think popular or large correlate with code quality. In fact it's probably the opposite. It uses pretty ancient c++ stuff, which immediately disqualifies it from bring of high quality in regards to the cpp code (and also is the cause for their security bugs)

TwentyPosts
0 replies
19h12m

It's pretty widely known that google is bad at c++

No, it's not. This is literally the first time I hear about this. Do you have any sources to support your claim, especially in light of the fact that Google has written and contributed to some of the largest and most important C++ codebases in existence?

bluGill
2 replies
20h0m

vector.at() is memory safe. You get a choice. Easy to ban [] if you cannot statically prove it is safe.

C++11 isn't the most memory safe language, but C++11 is a lot safer than older versions, and C++23 is better yet. I'm expecting true memory safety in C++26 (time will tell), but it will be opt-in profiles which isn't ideal.

z_open
1 replies
10h10m

This is not idiomatic C++ in any C++ standard. You can also just replace vector with map so the brackets insert, but that isn't either.

bluGill
0 replies
4h58m

Prefer at to [] is standard where I write C++. map is not standard because vector is almost always much faster random access in the real world (that is n is normally small enough that a linear search is faster than a binary search because of caching)

evouga
0 replies
1d

It's funny; I spent a couple of hours last week helping some students debug out-of-bounds indices in their Rust code.

I've written bugs that would have been caught by the compiler in a memory-safe language. I think the last time was maybe in 2012 or 2013? I still write plenty of bugs today but they're almost all logic errors that nothing (short of AI tools) could have caught.

girafffe_i
0 replies
15h38m

I heard about Rust recently.

devnullbrain
0 replies
19h23m

Oops, shared_ptr circular reference.

Oops, null smart pointer.

Oops, UB made my reference null.

Oops, invalidated iterator.

Oops, aliased pointers.

Oops, race condition.

Oops, recursion.

Oops, moved-from object is partially formed.

sk11001
15 replies
1d1h

Is it worth getting more into C++ in 2024? Lots of interesting jobs in finance require it but it seems almost impossible to get hired without prior experience (with C++ and in finance).

optimalsolver
13 replies
1d1h

Yes.

I switched from Python to C++ because Cython, Numba, etc. just weren't cutting it for my CPU-intensive research needs (program synthesis), and I've never looked back.

sk11001
12 replies
1d1h

My question isn't whether it's a good fit for a specific project, I'm more interested in whether it's a good career choice e.g. can you get a job using C++ without C++ experience; how realistic is it to ramp up on it quickly; whether you're likely to end up with some gnarly legacy codebase as described in the OP; is it worth pursuing this direction at all.

hilux
4 replies
1d1h

Did you see yesterday's article about the White House Office of the National Cyber Director (ONCD) advising developers to dump C, C++, and other languages with memory-safety issues?

avgcorrection
1 replies
21h40m

We’re still in for another 20 years of hardcore veteran Cxx programmers insisting that either the memory safety issue is overblown or just a theoretical issue if you are experienced enough/use a new enough edition of the language.

bluGill
0 replies
20h33m

The C++ committee is looking hard at how to make C++ memory safe. If you use modern C++ you are already reasonably memory safe - the trick is how do we force developers to not access raw memory (no new/malloc, use vector not arrays...). There are some things that seem like they will come soon.

Of course if you really need that non-memory safe stuff - which all your existing code does - then you can't take advantage of it. However you can migrate your C++ to modern C++ and add those features to your code. This is probably easier than migrating to something like Rust (Rust cannot work with C++ unless you stick with the C subset from what I can tell) since you can work in small chunks at a time in at least some situations.

sk11001
0 replies
1d1h

Yes, and at the same time I’m seeing ads for jobs that pay more than double what I make that require C++.

mkipper
0 replies
23h24m

I still think knowing C++ is pretty valuable to someone's career (at least over the next 10 - 15 years) if they're looking to work in fields that traditionally use C++ but might be transitioning away from it.

The obvious comparison is Rust. There are way more C++ jobs out there than Rust jobs. And even if I'm hiring for a team developing something in Rust, I'd generally prefer candidates with similar C++ experience and a basic understanding of Rust over candidates with a strong knowledge of Rust and no domain experience. Modern C++ and Rust aren't _that_ dissimilar, and a lot of ideas and techniques carry over from C++ to Rust.

Even if the DoD recommends that contractors stop using C++ and tech / finance are moving away from it, I'd say we're still years away from the point where Rust catches up to C++ in terms of job opportunities. If your main goal is employment in a certain industry, you'll probably have an easier time getting your foot in the door with C++ than Rust. Both paths are viable but the Rust path would be much harder IMO.

patrick451
1 replies
18h44m

IME, c++ was easier to ramp up on than typescript. C++ still a lingua franca in many domains, e.g., robotics, games, finance.

throwaway2037
0 replies
17h4m

Finance? No, most of it was rewritten in the 2000s to Java or DotNet. Sure, a bunch of HNers will reply here that they work on high frequency market making systems that use C++, but they are an extreme minority in the industry at this point.

jandrewrogers
1 replies
20h34m

Modern C++ is the language of choice for high-performance, high-scale data-intensive applications and will remain so for the foreseeable future. This is a class of application for which it is uniquely suited (C and Rust both have significant limitations in this domain). There are other domains like gaming that are also heavily into C++. Avoiding legacy C++ codebases is more about choosing where you work carefully.

It goes without saying that if you don't like the kinds of applications where C++ excels then it may not be a good career choice because it is not a general purpose language in practice.

throwaway2037
0 replies
17h7m

Modern C++ is the language of choice for high-performance, high-scale data-intensive applications

C and Rust both have significant limitations in this domain

Rust? Please provide concrete examples. I don't believe it.

TillE
1 replies
1d

C++ is really a language that you want to specialize in and cultivate years of deep expertise with, rather than having it as one tool in your belt like you can with other languages.

That's certainly a choice you can make, and modern C++ is generally a pretty good experience to work with. I would hope that there's not a ton of active C++ projects which are still mostly using the pre-2011 standard, but who knows.

sgerenser
0 replies
21h57m

This exactly. It’s a blessing and a curse, because I’d love to move to a “better” language like Rust or even Zig. But with 20+ years of C++ experience I feel like I’d be throwing away too much to avoid C++ completely. Also agreed that modern C++ is pretty decent. Lamenting that I’m back in a codebase that started before C++11 vs my previous job that was greenfield C++14/17.

stagger87
0 replies
20h51m

I would be very surprised if most people actually choose to develop in C++. It's a very good language choice for many domains, and I suspect interest and expertise in those domains drives people to C++ more than a desire to program in C++.

d_sem
0 replies
19h37m

Depends on the industry you are interested in entering.

My myopic view of the world has seen the general trend from C to C++ for realtime embedded applications. For example: in the Automotive Industry all the interesting automotive features are written in C++.

sealeck
6 replies
1d1h

rm -r

Problem solved

GuB-42
4 replies
1d

If you mean "rewrite from scratch", believe me, it is the worst thing you can do. I speak from experience, it is tempting but the few times I have done that, a few months later as I get burnt, I could only think of how an idiot who never learn I was.

Legacy code is like that because it went through many bugfixes and addressing weird requirements. Start over and you lose all that history, and it is bound to repeat itself. That weird feature that makes no sense, as it turns out, makes a lot of sense for some users, and that why it has been implemented in the first place. And customers don't care about your new architecture and fancy languages, they want their feature back, otherwise they won't pay.

Another way to look at it is when you asked to maintain a legacy code base, that's because that's software that has been in use for a long time. If it was that bad, it would have been dropped long ago, or maybe even cancelled before it got any use. Respect software that is used in production, many don't reach that stage.

Of course there are exceptions to that rule, but the general idea about rewriting from scratch is: "no" means "no", "maybe" means "no", and "yes" means "maybe".

bluGill
2 replies
20h14m

I have been involved in a successful rewrite. It cost billions of dollars and many years when the code wasn't working so the old system was still in use. We also ended up bringing over some old code directly just to get something - anything - functional at all. For many years my boss kept the old version running on his desk because when there was a question that old system was the requirements.

Today we only have to maintain the new system (the old is no longer sold/supported), and the code is a lot better than the old one. However I suspect we could have refactored the old system in place for less time/money and been shipping all the time. Now we have a new system and it works great - but we already have had to do significant refactors because some new requirement came along that didn't fit our nice architecture.

chx
1 replies
10h50m

because some new requirement came along that didn't fit our nice architecture.

This is the thing.

I was doing the event section of the website and it was the event. I mean, it was preposterous to even think of running multiple events. Fast forward a few years, the company is now many times the size after very rapid growth, has an office in the UK and now runs multiple events. Would you have made an architecture for multiple events back then? YAGNI whispers you not to...

bluGill
0 replies
4h47m

When considering this question, I've designed for things knowing they would come and they never did. I've also designed for situations that did come, but 10 years later we discovered a significant downside that we didn't anticipate and so the seemingly non-elegant hacks would have been better.

sealeck
0 replies
20h42m

I'm being like 1000% facetious, and agree that rewrites are bad.

dtx1
0 replies
1d1h

C++

Not even once
mk_chan
5 replies
22h17m

I’m not sure why there’s so much focus on refactoring or improving it. When a feature needs to be added that can just be tacked onto the code, do it without touching anything else.

If it’s a big enough change, export whatever you need out of the legacy code (by calling an external function/introducing a network layer/pulling the exact same code out into a library/other assorted ways of separating code) and do the rest in a fresh environment.

I wouldn’t try to do any major refactor unless several people are going to work on the code in the future and the code needs to have certain assumptions and standards so it is easy for the group to work together on it.

bluGill
1 replies
20h57m

The right answer depends on the future. I've worked on C++ code where the replacement was already in the market but we had to do a couple more releases of the old code. Sometimes it is adding the same feature to both versions. There is a big difference in how you treat code that you know the last release is coming soon and code where you expect to maintain and add features for a few more decades.

mk_chan
0 replies
11h47m

Yes, you have to expect the future (or even better if your manager/boss already has expectations you can adopt to begin with) and then choose the right way to tackle the changes required. That's why I laid out 3 possible cases the last of which points out that I prefer to refactor primarily when there's a lot of work incoming on the codebase. Personally, I don't see much value in refactoring code significantly if you alone are going to be editing it because refactoring for ease of editing + the cost of editing in the refactored codebase is often less than just eating the higher cost of editing in the pre-refactored codebase and you don't reap the scaling benefits of refactoring as much. However, like I mentioned in the above paragraph, it depends. In the end it's all about managing the debt to get the most out of it in a _relatively_ fixed time period.

FpUser
1 replies
19h31m

"When a feature needs to be added that can just be tacked onto the code, do it without touching anything else."

In few lucky cases. In real life new feature is most likely change in behavior of already existing one and suddenly you have to do some heavy refactoring in numerous places.

convolvatron
0 replies
17h29m

if you're going to own it for the foreseeable future. then own it. learn it, refactor it, test the hell of out of it. otherwise you're never going to be able to debug or extend it.

one thing I always do is throwaway major refactors. its the fastest way for me to learn what the structure is, what depends on what, and what's really kinky. and I might just learn enough to do it for real should it become necessary.

dj_mc_merlin
0 replies
21h48m

The post argues against major refactors. The incremental suggestions it gives progressively make the code easier to work with. What you suggest works until it doesn't -- something suddenly breaks when you make a change and there's so much disorganized stuff that you can't pinpoint the cause for much longer than necessary. The OP is basically arguing for decluttering in order to be able to do changes easier, while still maintaining cohesion and avoiding a major rewrite.

codelobe
5 replies
20h43m

My first thing is usually:

    #0: Replace the custom/proprietary Hashmap implementation with the STL version.
Once upon a time, C++ academics brow beat the lot of us into accepting Red-Black-Tree as the only Map implementation, arguing (in good faith yet from ignorance) that the "Big O" (an orgasm joke, besides others) worst case scenario (Oops, pregnancy) categorized Hash Map as O(n) on insert, etc. due to naieve implementations frequently placing hash colliding keys in a bucket via linked list or elsewise iterating to other "adjacent" buckets. Point being: The One True Objective Standard of "benchmark or die" was not considered, i.e., the average case is obviously the best deciding factor -- or, as Spock simply logic'd it, "The needs of the many outweigh the needs of the few".

Thus, it came to pass that STL was missing its Hashmap implementation; And since it is typically trivial (or a non issue) to avoid "worst case scenario" (of Waat? A Preggers Table Bucket?), e.g., use of iterative re-mapping of the hashmap. So it was that many "legacy" codebases built their own Hashmap implementations to get at that (academically forbidden) effective/average case insert/access/etc. sweet spot of constant time "O(1)" [emphasis on the scare quotes: benchmark it and see -- there is no real measure of the algo otherwise, riiight?]. Therefore, the affore-prophesied fracturing of the collections APIs via the STL's failure to fill the niche that a Hashmap would inevitably have to occupy came to pass -- Who could have forseen this?!

What is done is done. The upshot is: One can typically familiarize oneself with a legacy codebase whilst paying lip service to "future maintainability" by (albeit usually needless) replacing of custom Hashmap implementations with the one that the C++ standards body eventually accepted into the codebase despite the initial "academic" protesting too much via "Big O" notation (which is demonstrably a sex-humor-based system meant to be of little use in practical/average case world that we live in). Yes, once again the apprentice has been made the butt of the joke.

bluGill
2 replies
20h21m

In the mid 1990s when C++ was getting std::map and the other containers CPU caches were not a big deal. Average case was the correct thing to optimize for. These days CPU caches are a big deal and so your average case is typically dominated by CPU cache miss pipeline stalls. This means for most work you need different data structures. The world is still catching up to what this means.

codelobe
1 replies
19h30m

Well, Red-Black algos are supposed to be better at cache-locality, but I have an AVL-tree impl (ugg jokes, again: AVUL (ALV) is the "evil" tree of "forbidden" {carnal?} wisdom from The Garden of Eden, associated with Yggdrasil/Odin [a "pagan" God of Balance & Pleasure]) that has improved cache locality since its data nodes can be made to contain AvlTreeNode structure(s), and avoid copying any data, as users are made to provide node alloc/free function pointers to this C lib's Tree "constructor". This means, for real example, I have a command line option interpreter with const structures for each option, each node added to two AVL trees (to find by unicode codepoint and find by length prefixed unicode string name). C++ STL Map implementations can not conditionally generate code for const types and thus do needless coppies, whereas my C collections API causes 0 calls to malloc (vs STL's 2 mallocs per node insert). NodeAlloc is just pointer math to get at the apt AvlNode, NodeFree is NoOp.

Benchmarking the STL vs my AVL approach results in millions of times quicker cmd line opt interpretaion (for my gnu getopt replacement lib) due to reduction of pointer chasing...

And if I want to do something similar in C++ (overloading operator new), I have to instantiate multiple copies of the Tree code, one per each "class". What if I want to use my Sortable class with various allocators: OBJ cache, dynamic GC'd, static (no alloc, its in the .data section already)...? Well then I get N copies of EXACT SAME template code for no real reason, only differing in delete and new [con|de]structors. The cache-misses galore this causes isn't even fair to bench against the C w/ fn() ptr approach.

bluGill
0 replies
17h37m

Try benchmarking on something from 1995, like a 80486. Cache misses won't matter much.

sgerenser
0 replies
19h25m

It’s unfortunate that the hashmap picked by the standards comittee (std::unordered_map) is both awkwardly named and not very performant. Still probably better than whatever was hacked up in 1998, but nowadays you can do much better for any case where performance actually mattered. Note, still don’t roll your own, but there’s plenty of options from e.g. Abseil or Facebook’s Folly.

I worked on a project a few years ago where all data was stored in hashmaps. Just swapping out std::unordered map for an optimized implementation of a robin hood hash map increased performance by something like 2x and cut memory usage in half on many larger test cases.

samatman
0 replies
16h7m

that the "Big O" (an orgasm joke, besides others)

worst case scenario (Oops, pregnancy)

(of Waat? A Preggers Table Bucket?)

"Big O" notation (which is demonstrably a sex-humor-based system

This post reeks of obesity, desperation, poor life choices, and old-fashioned body odor.

bluetomcat
5 replies
1d1h

Been there, done that. Don't be a code beauty queen. Make it compile and make it run on your machine. Study the basic control-flow graph starting from the entry point and see the relations between source files. Debug it with step-into and see how deep you go. Only then can you gradually start seeing the big picture and any potential improvements.

johngossman
3 replies
1d1h

Absolutely. Read the code. Step through with a debugger. Fix obvious bugs. If it’s legacy and somebody is still paying to have it worked on, it must mostly work. Changing things for “cleanliness and modernization” is likely to break it.

cratermoon
2 replies
1d1h

Fix obvious bugs.

Be careful about that. Hyrum's Law and all.

johngossman
1 replies
18h12m

Should have been clearer. You’ve probably been put on the project because something isn’t working. Fix the simplest, most obvious of these. Fixing a bug is a good way to learn.

cratermoon
0 replies
12h28m

You’ve probably been put on the project because something isn’t working.

Perhaps if it's a change requested by the organization or the users. Just don't go "fixing" things that look like bugs without knowing if it's really a bug or expected behavior.

bluGill
0 replies
20h49m

In my experience it takes at least a year straight working with code before you can form an opinion on if it is beautiful or not. People who have not worked in a code base for that long do not understand what is a beautiful design corrupted by the real world vs what is ugly code. Most code started out with a beautiful design but the real world forced ugly on it - you might be able to improve this a little with full rewrite but the real world will still force a lot of ugly on you. However some code really is bad.

tehnub
4 replies
1d1h

I enjoyed the article and learned something. But I've been wondering: When people say "rewrite in a memory-safe language", what languages are they suggesting? Is this author rewriting parts in Go, Java, C#? Or is it just a smirky, plausibly deniable way of saying to rewrite it in Rust?

broken_broken_
1 replies
1d

Author here, thanks! A second article will cover this, but the bottom line is that it entirely depends on the team and the constraints e.g. is a GC an option (then Go is a good option), is security the highest priority, etc.

I’d say that most C++ developers will generally have an easy time using Rust and will get equivalent performance.

But sometimes the project did not have a good reason to be in C++ in the first place and I’ve seen successful rewrites in Java for example.

Apple is rewriting some C++ code in Swift, etc. So, the language the team/company is comfortable with is a good rule of thumb.

tehnub
0 replies
23h38m

Makes sense, thanks!

avgcorrection
1 replies
21h35m

So you saw a post about C++, it didn’t mention “Rust” once, mentioned “memory safe” languages which there are dozens of, and yet found a way to shoehorn in a dismissive comment about a meme. Nice.

We’ve reached the rewrite-in-rust meme stage of questioning whether the author is a nefarious crypto-Rust programmer in lieu of not being able to complain about it (since it wasn’t brought up!).

bsdpufferfish
0 replies
13h36m

(author actually shows up to advocate for rust)

leecarraher
4 replies
1d2h

create bindings and externalize function libraries for other languages, hope to your prefered deity nothing breaks

Night_Thastus
3 replies
1d1h

This adds additional problems. IE, Start replacing legacy C++ with Python, now debugging and following the flow of the code becomes very difficult.

bluGill
2 replies
20h4m

If it is C++ I wouldn't think about python in most cases. Rust should come to mind. Ada, or D are other options you sometimes hear about.

Night_Thastus
1 replies
14h22m

Is it possible to integrate any of those while allowing seamless debugging? IE, step right from one into another? I've yet to see that happen.

bluGill
0 replies
4h54m

If nothing else my C++ debugger will see the functions of everything in Rust, D, or ada - it might be a mangled name but generally I can figure them out. Once you step into python the debugger is going to see the python runtime functions and you need to dig into them to figure out what of your functions you are running.

I have yet to figure out how to get any language other than C++ into my system so I can't say how well it works in the real world. Then again I work on an embedded system with real time controls so I can rarely use a debugger since as soon as I hit a breakpoints all my must happen at time X functions fail to run and the whole system fails in a few ms.

sjc02060
3 replies
1d2h

A good read. We recently did "Rewrite in a memory safe language?" successfully. It was something that shouldn't have been written in C++ in the first place (it was never performance sensitive).

throwaway2037
0 replies
17h9m

Do you have a public write-up (blog post)? If yes, you should post it on HN. It would probably generate lots of interesting conversation.

tehnub
0 replies
1d1h

Would you mind sharing what language you used?

jstimpfle
0 replies
21h59m

Probably not a project spanning more than 3 decades of development and millions of lines of code?

dureuill
3 replies
19h59m

What do you do now?

Look for another job

You’d be amazed at how many C++ codebase in the wild that are a core part of a successful product earning millions and they basically do not compile.

Wow I really hope this is hyperbole. I feel like I was lucky to work on a codebase that had CI to test on multiple computers with WError

throwaway71271
2 replies
19h48m

Wow I really hope this is hyperbole.

I am sure its not, I dont have much experience as I have worked in only 3 companies in the last 25 years, but so far I have found no relation between code quality and company earnings.

throwaway2037
1 replies
17h1m

so far I have found no relation between code quality and company earnings.

This! What matters is the market fit and customer experience. You can deliver a lot of value with average programmers working on a shitty code base.

gmueckl
0 replies
14h52m

I started to joke that in order to have a successful software startup, you need to essentially write the most godawful program code you can get away with. The money is much better spent on a good/aggressive sales strategy.

Elegant technology never wins on its own merits.

VyseofArcadia
2 replies
22h7m

Get out the chainsaw and rip out everything that’s not absolutely required to provide the features your company/open source project is advertising and selling

Except every legacy C++ codebase I've worked on is decades old. Just enumerating the different "features" is a fool's errand. Because of reshuffling and process changes, even marketing doesn't have a complete list of our "features". And even it there was a complete list of features, we have too many customers that rely on spacebar heating[0] to just remove code that we think doesn't map to a feature.

That's if we can even tease apart which bits of code map to a feature. It's not like we only added brand new code for each feature. We also relied on and modified existing code. The only code that's "safe" to remove is dead code, and sometimes that's not as dead as you might think.

Even if we had a list of features and even if code mapped cleanly to features, the idea of removing all code not related to "features your company is advertising or selling" is absurd. Sometimes a feature is so widely used that you don't advertise it anymore. It's just there. Should Microsoft remove boldface text from Word because they're not actively advertising it?

The only way this makes sense is if the author and I have wildly different ideas about what "legacy" means.

[0] https://xkcd.com/1172/

lelanthran
0 replies
9h8m

> Get out the chainsaw and rip out everything that’s not absolutely required to provide the features your company/open source project is advertising and selling

Except every legacy C++ codebase I've worked on is decades old. Just enumerating the different "features" is a fool's errand. Because of reshuffling and process changes, even marketing doesn't have a complete list of our "features".

Yeah, this struck me also, and your post should be modded up more. Anyone with significant experience in development knows what "Legacy" means.

Regardless of the language, after a specific point in a product's lifetime you cannot "know" all the features. Just not possible, no matter how well you think you documented it.

In an old product, every single line of code is there because of a reason that is not in the docs. Some examples that I've seen:

1. Using `int8_t` because at some point we integrated a precompiled library that was compiled with signed char, and we want warnings to pop up when we mix signs.

2. Wrote our own stripped-down SSL library because OpenSSL was not EMV certified at the time and did not come with the devkit. Now callers depend on a feature in our own library.

3. Client calls our DLL with Windows-specific UTF-16 strings. That's why that function has three variants that take 3 different types of strings.

4. This library can't be compiled with any gcc/glibc newer than X.Y, because the compiled library is loaded with `dlopen` in some environments.

5. We have our own 'safe' versions of string functions, because MSVC which takes the same parameter types for those functions assigns different meanings to the `size` parameter.

6. Converting fixed-precision floats to an int, performing the additions, and then converting the last division is faster and more accurate, but the test suite at the client expects the cumulative floating point errors and the test will fail.

Not to mention uncountable "marketing said this is not offered as a feature, but client depends on it" things.

Palomides
0 replies
17h16m

hard agree

removing a feature is possibly the most politically intractable thing you can try to do with a legacy codebase, almost never worth trying

vijucat
1 replies
19h36m

Not mentioned were code comprehension tools / techniques:

I used to use a tool called Source Navigator (written in Tcl/tk!) that was great at indexing code bases. You could then check the Call Hierarchy of the current method, for example, then use that to make UML Sequence Diagrams. A similar one called Source Insight shown below [1].

And oh, notes. Writing as if you're teaching someone is key.

Over the years, I got quite good at comprehending code, even code written by an entire team over years. For a brief period, I was the only person actively supporting and developing an algorithmic trading code base in Java that traded ~$200m per day on 4 or 5 exchanges. I had 35 MB of documentation on that, lol. Loved the responsibility (ignoring the key man risk :|). Honestly, there's a lot of overengineering and redundancy in most large code bases.

[1] References in "Source Insight" https://d4.alternativeto.net/6S4rr6_0rutCUWnpHNhVq7HMs8GTBs6...

jimkoen
0 replies
5h27m

I used to use a tool called Source Navigator

I can't believe I'm finding someone in the wild that also has used Source Navigator.

My university forced this artifact on me in the computer architecture course because it has some arcane feature set + support for an ARM emulator that isn't found elsewhere. We used it for bare metal ARM assembly programming

summerlight
1 replies
16h29m

This thread has lots of good advice. I'll add some of mine, not limited to C/C++. If you have luxury of using VCS, make a full use of its value. Many teams only use it as a tool merely for collaboration. VCS can be more than that. Pull the history then build a simple database. It doesn't have to be an RDB (it's helpful though); a simple JSON file or even a spreadsheet file is a good starter. There are so many valuable information to be fetched with just a simple data driven approach, almost immediately.

  * You can find out the most relevant files/functions for your upcoming works. If some functions/files have been frequently changed, then it's going to be the hot spot for your works. Focus on them to improve your quality of life. If you want to introduce unit tests? Then focus on the hot spot. Suffer from lots of merge conflicts? The same.
  * You can also figure out correlation among the project and its source files. Some seemingly distant files are frequently changed together? Those might suggest an implicit structure that not might be clear from the code itself. This kind of information from external contexts can be useful to understand the bird's eye view.
  * Real ownership models of each module can be inferred from the history. Having a clear ownership model helps, especially if you want to introduce some form of code review. If some code/data/module seems to have unclear ownership? That might be a signal for refactoring needs.
  * Specific to C/C++ contexts, build time improvements could be focused on important modules, in a data driven way. Incremental build time matters a lot. Break down frequently changed modules rather than blindly removing dependencies on random files. You can even combine this with header dependency to score the module with the real build time impact. 
There could be so many other things if you can integrate other development tools with VCS. In the era of LLM, I guess we can even try to feed the project history and metadata to the model and ask for some interesting insights, though I haven't tried this. It might need some dedicated model engineering if we want to do this without a huge context window but my guts tell that this should be something worth try.

bostonvaulter2
0 replies
15h4m

Nice ideas! Do you have any tips for software to help automate some of those analyses?

professorTuring
1 replies
22h7m

How good is AI refactoring the code? Haven’t tried it yet, but… as someone who has need to work on tons of legacy in the past… looks interesting!

bluGill
0 replies
20h19m

Very mixed. Sometimes great, but you have too watch it close as once in a while it will do garbage.

There is a lot of non-AI refactoring for C++ these days that is very good. And many more tools that will point to areas that there is a problem and often a manual fix of those areas is "easy".

jcarrano
1 replies
20h28m

Good points, but this is not something you can solve with a recipe. Investigate, talk to people and make sure you are solving actual problems and prioritizing the right tasks.

This is an extremely crucial step that you must do first: familiarize yourself with the system, its uses and the reasons it works like it does. Most things will be there for a reason, even if not written to the highest standard. Other parts might at first sight seem very problematic yet be only minor issues.

Be careful with number 4 and 5. Do not rush to fix or rewrite things just because they look like they can be improved. If it is not causing issues and it is not central to the system, better spend your resources somewhere else.

Get the team to adopt good practices, both in the actual code and in the process. Observe the team and how they work and address the worst issues first, but do not overwhelm them. They may not even be aware of their inefficiencies (e.g. they might consider complete rebuilds as something normal to do).

bombcar
0 replies
20h27m

I did not find Chesterton's fence, and was sad.

The very first thing to do with a new codebase is don't touch anything until you understand it, and then don't touch anything until you realize how mistaken your understanding was.

ecshafer
1 replies
1d1h

This is pretty great advice for any legacy code project. Even outside of C++ there is a huge amount of code bases out there that do not compile/run on a dev machine without tons of work. I once worked on a Java project that due to some weird dependencies, the dev mode was to run a junit test which started spring and went into an infinite loop. Getting a standard run to work helped a ton.

bluGill
0 replies
20h23m

The difference between greenfield and legacy code is just a few years. So learn to work with legacy code and how to make it better over time.

bArray
1 replies
1d1h

3. Make the project enter the 21st century by adding CI, linters, fuzzing, auto-formatting, etc

I would break this down:

a) CI - Ensure not just you can build this, but it can be built elsewhere too. This should prevent compile-based regressions.

b) Compiler warnings and static analysers - They are likely both smarter than you. When it says "warning, you're doing weird things with a pointer and it scares me", it's a good indication you should go check it out.

c) Unit testing - Set up a series of tests for important parts of the code to ensure it performs precisely the task you expect it to, all the way down to the low level. There's a really good chance it doesn't, and you need to understand why. Fixing something could cause something else to blow up as it was written around this bugged code. You also end up with a series of regression tests for the most important code.

n) Auto-formatting - Not a priority. You should adopt the same style as the original maintainer.

5. If you can, contemplate rewrite some parts in a memory safe language

The last step of an inherited C++ codebase is to rewrite it in a memory safe language? A few reasons why this probably won't work:

1. Getting resources to do additional work on something that isn't broken can be difficult.

2. Rather than just needing knowledge in C++, you now also need knowledge in an additional language too.

3. Your testing potentially becomes more complex.

4. Your project likely won't lend itself to being written in multiple languages, due to memory/performance constraints. It must be a significantly hard problem that you didn't just write it yourself.

5. You have chosen to inherit a legacy codebase rather than write something from scratch. It's an admittance that you don't have some resource (time/money/knowledge/etc) to do so.

jpc0
0 replies
10h3m

The last step of an inherited C++ codebase is to rewrite it in a memory safe language

Simply getting rid of any actually memory unsafe C++ and enforcing guidelines will do this for you in the C++ codebase.

"Rewrite it in X" only adds complexity because it's the flavour of the month as you said in your comment.

Author is already doing the work of rewriting large chunks of the codebase in C++, they may as well follow and implement a more restrictive subset of the language, I find High integrity C++ to be good. If I can get my hands on the latest MISRA standard that is likely good as well. These may not be "required" but they specify what is enforced in <enter "safe" language here>. So instead of having to reskill your entire devteam on a new language which has many many sharp edges, how about just having your dev team use the language they already know and enforce guidelines to avoid known footguns.

xchip
0 replies
8h9m

Write unit tests to make sure your refactoring work.

It better, change job to do something more interesting

w10-1
0 replies
16h32m

Once you have the SCM in order, and before you make any changes:

Structure101 is the best way to grok the architecture er tangles of a large code base. They have a trial period that would give you the overview, but their refactoring support is fantastic (in Java at least).

https://structure101.com/products/workspace/

throw_m239339
0 replies
14h17m

I quit. Life is short.

t43562
0 replies
8h46m

I think the very best thing one can do is reduce the amount of variation you have to support. The burden of change is thus vastly reduced and the number of possible avenues for improvement explodes.

We could have left customers with old operating systems on the older versions of the product. A lot of them never upgraded anyhow. We absolutely destroyed our productivity by not making this kind of decision. We also really hurt ourselves by supporting Windows - as soon as there are 2 or more completely different compilers things turn to **t. I'm not even sure we made much money from it.

Given the ability to use new tools (clang, gcc and others) that are only available on newer operating systems we could have done amazing things. All those address sanitizers etc would have been wonderful and I would like to have done some automated refactoring which I know clang has tools for.

Most of the problems were just with understanding the minds of the developers - they were doing something difficult and at a level of complexity that somewhat overmatched the problem most of the time but the complexity was there to handle the edge cases. I wanted to go around adding comments to the files and classes as I understood bits of it. I was working with one of the original developers who was of course not at all interested in anyone understanding it or making it clearer and this kind of effort tended to get shot down.

If you don't have good tests you're dead in the water. I have twice inherited python projects without tests at all and those were a complete nightmare until I added some. One was a long running build process in which unit tests were only partially helpful. Until I came up with a fake android source tree that could build in under a minute I was extremely handicapped. Once I had that everything started to get much better.

My favorite game ... is an open source C++ thing called warzone2100 - no tests. It's not easy to make changes with confidence. I imagine to myself that one day my contribution will be to add some. The problem is that I cannot imagine the current developers taking all that kindly to it. Some people get to competence in a codebase and leave it at that.

sega_sai
0 replies
21h33m

To be honest a lot of recommendations apply to other languages as well. I.e. start with tests only then change, add autoformatting etc. At least I had experience of applying a similar sequence of steps to a python package.

rurban
0 replies
11h26m

I just went this very same dance with an old project, smart, which evaluates string matching algorithms. Faster strstr(). From 2013. It was in a better shape than zlib, but still.

Their shell build script was called makefile, kid me not. So first create a proper dependency management: GNUmakefile. A BSD makefile would have been prettier, but not many are used to this. dos2unix, chmod -x `find . -name *.c -o name *\.h`, clang-format -i All in seperate commits.

Turns out there was a .h file not a header, but some custom list of algorithm states, broken by fmt. Dontg do that. Either keep it a header file, or rename it to .lst or such.

Fix all the warnings, hundreds. Check with sanitizers. Check the tests, disable broken algorithms, and mark them as such.

Improve the codebase. There are lots of hints of thought about features. write them. Simplify the state handling. Improve the tests.

Add make check lint. Check all the linter warnings.

Add a CI. Starting with Linux, Windows mingw, macos and aarch64. Turns out the code is Linux x64 only, ha. Make it compat with sse checks, windows quirks.

Waiting for GH actions suck, write Dockerfiles and qemu drivers into your makefile. Maybe automake would have been a better idea after all. Or even proper autoconf.

Find the missing algorithms described elsewhere. Add them. Check their limitations.

Reproducible builds? Not for this one, sorry. This is luxury. Rather check clang-tidy, and add fuzzing.

https://github.com/rurban/smart

rayiner
0 replies
3h9m

This is excellent advice, especially the list of what not to do. I don’t think it’s just C++, it’s just C++, it’s working with any legacy code base. You gotta approach it on its own terms, and analyze and fully understand what’s happening before you start changing things.

I observed from afar when the Gwydion Dylan folks (the Dylan successor to the CMU CL compiler) inherited Harlequin’s Dylan compiler and IDE and decided to switch to that going forward: https://opendylan.org. The work (done out in the open in public mailing lists and IRC) is a very nicely done case study in taking a large existing code base developed by someone else, studying it, and refactoring it to bring it incrementally into the present. They started with retooling the build system and documenting the internals. Then over time they addressed major pain points, like creating an LLVM backend to avoid the need to maintain custom code generators.

pvarangot
0 replies
19h17m

Besides what everyone else told you make sure you are making at least 250k/y

nottorp
0 replies
7h20m

Most people resort to using the system package manager, it’s easy to notice because their README looks like this:

... and goes on against using system packages.

Well if you're not using what your OS provides, why don't you statically link?

After all, it's the customer who pays for the extra storage and ram requirements, not you.

myrmidon
0 replies
1d1h

Really liked it! Especially the "get buy in" is really good advice-- always stressing how the effort spent on refactoring actually improves things, and WHY its necessary.

Something that's kinda implied that I would really stress: Establish a "single source of truth" for any release/binary that reaches production/customers, before even touching ANY code (Ideally CI. And ideally builds are reproducible).

If you build from different machines/environments/toolchains, its only a matter of time before that in itself breaks something, and those kinds of problems can be really "interesting" to find (an obscure race condition that only occurs when using a newer compiler, etc.)

m_a_g
0 replies
6h3m

I don't want to be that person, but I'd change teams or move to a different company.

legacybob
0 replies
8h27m

Every morning I wake up in my legacy bed, before taking breakfast using a legacy coffee cup. I then take a shower using - you guessed it - legacy shower taps (after all, I do live in a legacy building).

I then sit on my legacy chair to browse the Internet and read about brand new programming things (through a legacy monitor).

jxramos
0 replies
9h42m

I like to use cppdepend to navigate a large and unfamiliar codebase https://www.cppdepend.com. The interactive dependency graph and integration to the editor to jump back and forth in diagrams to actual code and the many logical constructs in the source certainly accelerates getting a quick sense of the layering and a bit of the architecture of the project.

jujube3
0 replies
21h16m

Look, I'm not saying you should rewrite it in Rust.

But you should rewrite it in Rust.

joshmarinacci
0 replies
20h1m

You need to install linters and formatters and security checkers. But you need to start using them incrementally. Trying to fix all the issues found at once is a quick recipe for madness. I suggest using clang-tidy with a meta-linter like Trunk Check

docs:

https://docs.trunk.io/check/configuration/configuring-existi...)

jeffrallen
0 replies
22h21m

This was my job at Cisco. But it was a C code base, which used nonstandard compiler extensions, and so could not be built without the legacy compilers with their locally made extensions. Also the "unit tests" were actually hardware-in-the-loop tests. And the Makefiles referenced NFS filesystems automounted from global replicas, but none of them were on my continent.

Fun times. Don't work there anymore. Life is good. :)

ilitirit
0 replies
12h24m

I've had to do this several times in the past. Honestly, my best advice would probably be make several backups, then to do as little as possible. If you need to make a small change, fine. Bigger changes? Consider if you can't do the bulk of the work in a technology or stack you understand and only make a small change to the legacy code base.

Most of the time I spend with C++ code revolves around figuring out compile/link errors. Heaven forbid you need to deal with non-portable `make` files that for some reason work on the old box, but not yours... Oh, and I hope you have a ton of spare space because of some reason building a 500k exe takes 4GB.

Keep in mind, this advice only applies to inherited C++ code bases. If you've written your own or are working on an actively maintained project these are non-issues. Sort-of.

huqedato
0 replies
1d1h

Whenever I inherited a project containing legacy code, regardless of the frameworks, tools, or languages used, we always found it necessary to drop it and begin anew. Despite my efforts to reuse, update, or refactor it, we inevitably reached a point where it was unusable for further development.

hesdeadjim
0 replies
1d1h

Start grinding leetcode and find another gig?

happyweasel
0 replies
5h52m

If you have a codebase with lots and lots of tests, you are not in a bad place. Remember legacy means a codebase that works and solved and still solves problems over decades. In a sense,a successfull software project implies it will be marked as legacy. Always prefer legacy over Hype.

grandinj
0 replies
1d

This is generally the same path that LibreOffice followed. Works reasonably well.

We built our own find-dead-code tool, because the extant ones were imprecise, and boy oh boy did they find lots of dead stuff. And more dead stuff. And more dead stuff. Like peeling an onion, it went on for quite a while. But totally worth it in the end, made various improvements much easier.

girafffe_i
0 replies
15h39m

Rewrite it in Rust.

girafffe_i
0 replies
15h21m

Lol no one reads, just RIIR.

geoelectric
0 replies
18h44m

If the blog author is lurking on here, I tried to bookmark the article since it's very relevant to my current situation, but it didn't have a title set for the page. NBD to copy/paste one in, but it took me by surprise.

elzbardico
0 replies
20h57m

1990 Windows C++ code? Consider euthanasia as a painless solution.

dirkc
0 replies
10h19m

My take:

1. Source control

2. Reproducible builds

3. Learn the functionality

4. ...

If you don't understand what the code does, you're probably going to regret any changes you make before step 3!

delta_p_delta_x
0 replies
19h50m

So what do I recommend? Well, the good old git submodules and compiling from source approach.=

It is strange that the author complains so much about automating BOMs, package versioning, dependency sources, etc, and then proceeds to suggest git submodules as superior to package managers.

The author needs to try vcpkg before making these criticisms; almost all of these are straightforwardly satisfied with vcpkg, barring a few sharp edges (updating dependencies is a little harder than with git submodules, but that's IMO a feature and not a bug—dependencies are built in individual sandboxes which are then installed to a specified directory. vcpkg can set internal repositories as the registry instead of the official one, thus maintaining the 'vendored in' aspect. vcpkg can chainload toolchains to compile everything with a fixed set of flags, and allows users to specify per-port customisations.

These are useful abstractions and it's why package managers are so popular, rather than having everyone deal with veritable bedsheets' worth of strings containing compile flags, macros, warnings, etc.

davidw
0 replies
1d1h

Well, tomorrow is the "who's hiring?" thread...

cloudhan
0 replies
15h47m

You run or your code runs, choose one and choose it wisely ;)

cljacoby
0 replies
2h52m

Despite being framed as something for legacy C/C++ codebases, this is pretty good advice for setting up testing and CI automation around any project.

I recently started on a new Rust project, and despite not having to worry about things like sanitizers as much, I followed a similar approach of getting it to compile locally, getting it compile in a docker container, setup automated CI/CD against all PRs.

Although I would order the steps as 1, 3, 4, 2. Don't get out the chainsaw until you have CI/CD tests evaluating your code changes as you go.

btbuildem
0 replies
3h42m

Get the build working on your machine

Nope. Make a portable/virtualized env, and make it build there. That way it'll build on you machine, in the CI pipeline, on your co-worker's machine, etc etc.

linters, fuzzing, auto-formatting

No, for at least two reasons:

1) Too risky, you change some "cosmetic" things and something will quietly shift in the depths, only to surface at the worst possible time

2) Stylistic refactors will bury the subtle footprints of the past contributors - these you will need as you walk through the tunnels on your forensic missions to understand Wat The Fuk and Why

Generally, touch as little as possible, focus on what adds value (actual value, like, sales dollars). The teetering jenga tower of legacy code is best not approached lightly.

Work with PMs to understand the roadmap, talk to sales and support to understand what features are most used and valuable. The code is just an artifact, a record of unhappy accidents and teeth-grinding compromises. Perhaps there's a way to walk away from it altogether.

bobnamob
0 replies
20h22m

This article (and admittedly most comments here) doesn't emphasize the value of a comprehensive e2e test suite enough.

So much talk about change and large LoC deltas without capturing the expected behavior of the system first

bfrog
0 replies
2h29m

How legacy we talking? Needs turbo C++ legacy?

beanjuiceII
0 replies
22h22m

the whitehouse says i should RiiR

asah
0 replies
7h20m

I love how HNers assume there are automated tests with any amount of test coverage. So cute!

...but grandpa, how did you know your code worked? Son, we didn't. <silence>

(wait until they hear that source control wasn't used...)

Scubabear68
0 replies
1d

The “rip everything out” step is not recommended. You will break things you don’t understand, invoke Chesterson’s Fence, and create enormous amounts of unnecessary work for yourself.

Make it compile, automate what you can, try not to poke the bear as much as you can, pray you can start strangling it by porting pieces to something else over time.

Merik
0 replies
21h19m

if you have access to Gemini Pro 1.5 you could put the whole code base into the context and start asking questions about architecture, style, potential pain paints etc.

Kon-Peki
0 replies
20h22m

The article doesn't mention anything about global variables, but reducing/eliminating them would be a high priority for me.

The approach I've taken is, when you do work on a function and find that it uses a global variable, try to add the GV as a function parameter (and update the calling sites). Even if it's just a pointer to the global variable, you now have another function that is more easily testable. Eventually you can get to the point where the GV can be trivially changed to a local variable somewhere appropriate.

Kapura
0 replies
1d1h

Get out the chainsaw and rip out everything that’s not absolutely required to provide the features your company/open source project is advertising and selling

Great advice! People do not often think about the value of de-cluttering the codebase, especially _before_ a refactor.

JoeAltmaier
0 replies
3h54m

Read it. A little every day until you've passed your eyes over all of it.

Make notes about mysterious things. Check them off once you've figured them out.

Try to find the 'business case' database. You will fail, nobody has one. Make one then, while you're reading the code.

Jean-Papoulos
0 replies
11h4m

A lot of this assumes the codebase is testable, but most of those legacy applications rely on global state a lot...

FpUser
0 replies
19h26m

And I have at some point inherited 5000+ files long legacy PHP code. I had to write Python program to parse that insanity to look for particular patterns and report those for manual fixing or do it automatically if possible. The example would be database access. That single software used 5 different methods to access it.

So no. I would not call C++ any special in this regards.

EvgeniyZh
0 replies
17h6m

RiiR

Dowwie
0 replies
6h49m

Immediately rewrite everything in Rust.

BlueTemplar
0 replies
12h4m

If you’re not doing [CI] already as a developer, I don’t think you really have entered the 21st century yet.

Ah yes, nothing like a veiled insult towards the people that might need that advice the most, blaming them for not using a developer paradigm that you even fail to name and for which you only give a hard to (directly) search two-letter acronym ! (/s)

( CI stands for Continuous Integration :

https://en.wikipedia.org/ wiki/Continuous_integration )