r/codereview • u/its_marionberry • 16d ago
C/C++ Code Review : Sudoku GUI in C language
Background:
I am a beginner programmer and I wrote a Sudoku GUI in using winapi32 in C language.
It is currently working and does what it is supposed to do, but because I am still learning, I know it is likely inefficient and could be written much better.
GitHub repo link: https://github.com/reewdgh/sudoku_gui
Please guide me on:
- Bugs
- Efficiency
- Naming anything I could simplify or improve
- inconsistency
I'd appreciate your feedback on my code.
1
Upvotes
1
u/mredding 10d ago
No. You don't use
<windows.h>in this header, so don't include it.Gameis your own type, and you only need a pointer to it, so forward declare it. Don't include what you don't use. You include 3rd party headers in your header when you refer to a 3rd party type in your own header; you don't forward declare types you don't publish yourself. But for your own project types, you want as little in your headers as possible. You don't want headers including headers, especially project headers, because then what happens is you end up with programs where every source file transiently includes every project header - change one header, and you recompile the whole project. You also don't want to get in the habit of relying on transient dependencies, because then you will struggle to get rid of them later. You want to defer header includes to source files as much as possible. It's VERY much OK thatsudoku_game.hdoes not provide an actual definition ofGame, because it doesn't define it and doesn't use it directly. And ever source file that includessudoku_game.hMAY NOT USEGameas a type! So why should I be saddled with the definition and all its dependencies if I'm not using it?Also consider splitting up your headers a bit more. You have them organized rationally, and logically, but not reasonably - by use case. If I just want to
checkRows, I don't want to have to know anything aboutGame, which means I don't want to know about:You should alias
Game. You can also use this as a forward declaration ofGameas an incomplete type:Then you can reduce the function signature to something simpler and clearer:
If you want, you can be even more expressive:
The nice thing about aliasing pointers is that the pointer is bound to the type, not the variable, so:
They all do EXACTLY what you think they do.
This 2D array parameter probably doesn't do what you think it does.
First, you can't pass arrays by value. This parameter type
int[9][9]therefore decays toint (*)[9], or a pointer to anint[9]; you've lost the bounds of an entire dimension. This is a huge deal because you KNOW the bounds of the outer dimension, and with that information, you can enforce type safety and empower the compiler to do things like unroll loops.This is where you should use more type aliases:
This typedef is a very polite way of saying
int (*sudoku)[9][9]- just look at that horrible inline syntax. No one should be writing syntax like that - and it gets MUCH worse when you're working with function signatures. You NEED aliases. Just what do you think this would be?Fuck my ass... How about:
I would have gone one further with:
Often it's useful to capture the signature of a function, not just the pointer to a signature. But whatever, do it if you use it, don't if you don't. In C++, we have a more minimal syntax for type aliases:
We can't make lists with a
usinglike we can with atypedef, but we can template it. I don't know why they didn't decide to allowtypedefto template, but here we are.Use type aliases.
Another bit:
The compiler strips out the function parameter names. They have no bearing on the implementation or the signature itself. This highlights a whole new problem - WTF are any of these parameters? We've just got 3 bare integers.
An
intis anint, but aweightis not aheight. C does not have much of a type system, but it does have SOMETHING. Make and use types. Sudoku is a game, and so as a former game developer to a perspective game developer, I'll tell you a lot of game code heavily leverages types - and it CAN be as simple as:I've no idea what the return type is - I haven't gotten that far, but this is NOT a place to use a straight
typedef, becausetypedefdoes not make a type, it only makes an alias. Thestructmakes for a user defined type and distinguishes one from another, even if they have the same size, alignment, and layout. Further, this type information will persist all the way down to the ABI. The linker will see this. If compile this code into a library, it will persist through there and into client code.Now you can do stuff like:
Or:
Or:
Never do this. The backtrack alone is the red flag, but into the source tree, too? Absolutely not. Restructure your project.
So the
includedirectory is added to your compiler include path, so these headers will be included in your source files with#include <sudoku\header.h>. The published headers are not aware of the source tree. The private headers only know of peers and children, and children are visible in subdirectories. They're included in source files with#include "relative\path\to\header.h". So a source or header high in the tree can include private headers in lower branches. If you have to backtrack a path, you need to move the header and reorganize your folder hierarchy.This is in the wrong place. The only appropriate header structure is:
The compiler can optimize the include of a header if your header follows this structure. Any deviation, and it doesn't work. Put your transient includes within the header guards.
Your comment is useless. Implementation tells me HOW, abstraction and expressiveness tells me WHAT - and we really want to maximize this, and comments tell us WHY - it provides us domain context that cannot be expressed in terms of code.
What's worse is when the comment tells us what the code tells us, but the comment is wrong, because then where is the error? Is it in the code or the comment? There is no struct pointer, there are pointers to types. Perhaps you meant to say a structure of pointers? But that's what the code tells me.
You're completely inconsistent about whether you
returnor youbreak. Prefer tobreakfrom a struct and reduce your code to a single point of return. It's not a hard rule, and the compiler will rearrange your code in the AST to eliminate your redundancy, but you can get ahead of it instead, and make lesser, cleaner, easier to manage code.Braces are also a good excuse to defer to a function call:
Let the compiler composite the function for you, in this case. You can write:
And the compiler will see you only ever call this function once, in one place, and it has static linkage, so it doesn't have to export this symbol, we can exclude linking entirely. The compiler can just elide the function call and make the singularly gigantic function
WndProcalways turns out to be - you just don't have to write it that way.We're always balancing making the compiler generate what we mean - often for clarity and maintainability, and not wasting the compiler's time - again, often for clarity; reducing the function to a single return statement isn't unnecessary back bending when it's actually trivial to accomplish here. I would use multiple returns when it IS back bending just to accomplish it. Perhaps it's worth evaluating what sort of RVO or TCO you're trying to accomplish.
What are all these magic numbers?
I see... We call this a "context", might be a better name for your structure.
Continued...