Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add chapter with C++ recomended practices #101

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

3x380V
Copy link

@3x380V 3x380V commented Aug 29, 2024

Converted from a document originally posted on FreeCAD forum: https://forum.freecad.org/viewtopic.php?t=89913 (reason to open PR is given by a moderator)
You can see preview here: https://www.triops.cz/fc/codeformatting/c++practices.html

While there are many threads of the forum about coding style, Modernisation is probably the most representative one, nothing reached FreeCAD documentation. This is an attempt to change that.

Comments welcome!

worser and others added 2 commits August 29, 2024 13:03
@chennes
Copy link
Member

chennes commented Aug 29, 2024

There's a lot of good advice in here, so overall this is a welcome addition to the handbook. My first suggestion is to add a prominent link at the top to the C++ Core Guidelines, which are as close as C++ comes to having "official advice."

To my reading, this document falls into the trap of assuming that the only useful tests are "unit" tests, of which we have essentially zero in our codebase. Very close to 100% of our test suite consists of integration and regression tests. The Python side of it by definition, and the C++ side by construction. And if you want to get into a fight on the internet, propose a definition of "unit" :).

This leads to things like the admonition not to use anonymous namespaces (because you can't write tests for that code directly), versus the CG https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rs-unnamed2. Avoiding anonymous namespaces because you can't write unit tests for that code doesn't seem relevant to this codebase.

Along the same lines, "New code should be delivered with unit tests" is not a recommendation we actually make here. Tests are always appreciated, but in almost no circumstances do we actually expect a "unit" test, and I can't see us suddenly moving to a situation in which we only merge code that has such tests. TDD is a specific development strategy that some people love and others hate: I don't see the FreeCAD team deciding to tell our (volunteer!) developers what process they must use to generate their code. That said, if you'd like to develop code that way, and ship the unit tests with your PR, by all means do so! I'm personally happy with any sort of test, unit or otherwise.

So suggest dropping the references to, and advice about, "unit tests", and shift to language talking about tests in a more general sense. Otherwise, this looks good to me.

@3x380V
Copy link
Author

3x380V commented Aug 29, 2024

@chennes, I'm just a messenger here as forum moderator does not wish a debate about code on the forum. Therefore I converted original document, which was deleted from the forum, to the markdown and put it there.

I'm not going to get into any kind of fight, I'd like to get a conclusion. As it comes to C++ Core Guidelines, probably just put all C++ stuff into already existing c++.md and also reference it from here?

@chennes
Copy link
Member

chennes commented Aug 30, 2024

I think the "Code Formatting" section will go away soon: in an ideal (and hopefully soon-achieved!) world we will have the vast majority of our code auto-formatted by clang-format and Black, so there's no point in having a handbook section about it. I do think it's completely reasonable to have a standalone "Best Practices in Language X" document, though. I'm open to suggestions for how to organize the repo, I don't feel strongly about it.

@3x380V
Copy link
Author

3x380V commented Nov 3, 2024

How far in the future is that point of "Code Formatting" section being away? Since I didn't write that chapter I propose leaving offending commit as it is with following modification commits summarizing your reply. Would that work for you?

Copy link
Member

@kadet1090 kadet1090 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this seems more like manifesto to me than useful guidelines. Proposed practices are not established in FreeCAD code base, lack concrete examples and sometimes even good explanation why it is the better way. Guidelines in such form will not be helpful to any new developer and can potentially scare people of because of language used. We should be welcoming for people and language used here is very much intimidating.

Comment on lines +91 to +110
Let’s make the data more expressive, more self-contained, and use STL
algorithm `find_if`:
{% raw %}
```cpp
using Pair = std::pair<std::string_view, double>;
constexpr std::array<Pair, 5> prefs {{
{ "", 0 },
{ "k", 1e3 },
{ "M", 1e6 },
{ "G", 1e9 },
{ "T", 1e12 },
}};

auto comp = [&](auto pair) { return pair.second <= input; };

const auto spec = std::find_if(prefs.rbegin(), prefs.rend(), comp);

auto prefix = spec->first;
```
{% endraw %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honestly not convinced that the find_if here is more readable. For me this code is clever, and clever code requires brain power to parse and understand how it works. First you need to understand what rbegin and rend means, second you have to lookup what does the comp function (awfuly named, btw), third you have to understand what pair.second means. On the other hand in previous example you have great names for every variable.

Not saying that utilities like std::find_if have no use, but this is not a good example. You could improve it substantially by using structured binding mentioned before, but honestly I don't think that it is worth.

FreeCAD code should be accessible for all people, and STL is very bad in terms of being easy to understand.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That very idea of inclusiveness without borders is the root of decay, but more importantly it really does not help anyone. Working with naive code is a pain once you are familiar with using brainpower and does not help those who aren't to improve. So yes, it is C++, a choice already made and efficient C++ is full of containers and STL. The sooner people find out, the better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not inclusiveness. Simple code is simpler to maintain, that's it. I don't think that in this particular case using STL makes it simpler. You should aim to better express the intent of the code and this fails spectacularly as I need more brainpower to understand it. As I said, you can improve it quite a lot by simply naming things better, using structured binding instead of unnamed pairs - then it might be better. But if example is provided it should be clearly better.

It's not like I am against STL - contrary I think that we underuse it by quite a lot and a lot of places . If it makes the code better - sure go ahead, there are plenty of examples where it would be useful. But don't use STL constructs where doing simple loop would be good enough.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should aim to better express the intent of the code

But isn't this exactly what find_if does? For a reviewer it's immediately clear that something is searched in a container and the next look is at the passed function or lambda expression to see the search criterion.

With the old style by iterating in a for-loop the intention usually is less clear and it leads to some conflation of code and data. And it's one of the goals of the C++ handbook to write better code, isn't it? Furthermore, the code is also less efficient when using a for-loop because after each iteration step a new iterator object will be created.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, you know that we are searching for something but to guess what you are trying to find and what is the intended result you have to think, a lot.

If anything, this example should be more like this:

struct SIPrefixSpec {
    std::string_view prefix;
    double magnitude;
};
constexpr std::array<SIPrefixSpec, 5> prefixes {{
	{ "", 0 },
	{ "k", 1e3 },
	{ "M", 1e6 },
	{ "G", 1e9 },
	{ "T", 1e12 },
}};

auto hasLowerMagnitude = [&](auto prefix) { return prefix.magnitude <= input; };

const auto highestPrefixSpec = std::find_if(prefixes.rbegin(), prefixes.rend(), hasLowerMagnitude);

auto prefix = highestPrefixSpec->prefix;

Then I can agree that it is a bit easier. However it still contains implicit reference to input in lambda, I'd much prefer something like this:

auto magnitudeLowerThan = [](auto value) {
 return [&](auto prefix) { return prefix.magnitude <= value; };
}

const auto highestPrefixSpec = std::find_if(prefixes.rbegin(), prefixes.rend(), magnitudeLowerThan(input));

As said previously, rbegin is also non-ideal as the r is really easy to miss, I'd rather order array correctly in the first place:

constexpr std::array<SIPrefixSpec, 5> prefixes {{
	{ "T", 1e12 },
	{ "M", 1e6 },
	{ "k", 1e3 },
	{ "G", 1e9 },
	{ "", 0 },
}};

Then standard begin and end could be used:

const auto highestPrefixSpec = std::find_if(prefixes.begin(), prefixes.end(), magnitudeLowerThan(input));

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps each of us is looking at the problem from different perspective. Let's look on practical example as shown in FreeCAD/FreeCAD@4a96d8c. Here, the original code clearly requires almost no brain power. Knowing basic C or even basic English is sufficient enough to understand it. Now, what could be the most common modification of that code required? Adding yet another unit probably. And that is far more straightforward with modified code. Looking to C++ as C on steroids and not object oriented language is what usually justifies all these for loops and ifelses...
Btw, that code snipped from example is humanReadableSize sitting in src/Mod/Start/App/DisplayedFilesModel.cpp

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked example is very good and in that case I absolutely agree that it is a big improvement. I am commenting however on example provided here which is not very good to illustrate why using find_if is better

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kadet1090, for a start I opened FreeCAD/FreeCAD#17817 with approximately the same code we are discussing here. Feel free to review it, suggest changes. Once agreed upon, we can eventually transfer it back here as an example.

Comment on lines +138 to +158
**Every branch in code doubles the number of paths. Increases complexity,
maintenance burden**.

Conditionals create indentation, and often violate the “Tell-Don’t-Ask”
principle. Conditionals may indicate laziness, naive structure, not using
polymorphism, design patterns etc.

Simple `if else` may be better expressed as a ternary. `else` is rarely
necessary. Code will be better without it. Ternary is ok though.

Even modestly complex code in an `if` or `else` branch should be
extracted to a function or lambda.

A sequence of conditionals stepping through related variables may indicate
code and data conflation.

Can also apply to `switch` statements. See also: repetition, variable sets, raw loops.

Complicated `if else` code might benefit from converting to state machine.

One school of thought says a function should have no more than one conditional,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is way too generic to be used in the guidelines. It feels more like manifesto than helpful guidelines. I'm also in anti-if camp but this needs more concrete examples. We need guidelines that are easy to follow and apply.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, I'm just a messenger here. All this used to be more specific and all this is a direct response to the code found in FreeCAD repository. It originated from forum discussion and it came with examples. All those posts (and threads) were deleted by a moderator as offensive. There is nothing I can do about it, maintainers of FreeCAD are expected to come to conclusion here.

Comment on lines +166 to +168
`const` is a statement of intent that something is to be immutable
(not able to change). Immutability aids reliability. In Rust immutability
is the default.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lacks explanation why we should make things const and why immutability is important.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How far in the future is that point of "Code Formatting" section being away?

Here is a PR FreeCAD/FreeCAD#16077 to auto-format our code base (excluding 3rd party code). In the 2nd commit all files are listed that the pre-commit script has touched.

But one still has to go through all the files and look what changes are made because sometimes the clang-format makes the code to look worse and less readable than before. These code parts should be excluded beforehand (with clang-format off/on).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lacks explanation why we should make things const and why immutability is important.

Well, it helps to reduce possible mistakes by a developer. See also: https://isocpp.org/wiki/faq/const-correctness
And last but not least the compiler will be able to do better optimization.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's contrary to the @chennes' remark about "Code Formatting" section going away. I do agree, sometimes manual formatting looks better, but it is highly subjective how it should look like. Therefore some guidance in "Code Formatting" section is still useful. I have no strong feeling about it, I was trying to get idea, how to organize this document - and now I'm more confused :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My basic point is that I don't want developers (or maintainers) to have to consider, or argue about, code formatting. By saying "use clang-format and Black unless there are exceptional reasons not to" we can avoid a lot of that sort of thing. I have no problem with occasional use of the format-disabling comments, but they should be rare, and the exceptions are probably not documentable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. But that still needs to be documented somewhere, something like "how to disable autoformat" and this still belongs to "Code Formatting" section which explains autoformatting is on by default. I'll try to add such a preparational commit about that, since I'm expecting autoformatting in use before we deal with the rest of comments :)

Comment on lines +201 to +206
Code cannot function, be understood, be edited or tested, without
its dependencies. So code free from dependencies is worth striving for.
Code and its dependencies are said to be _coupled._

When different pieces of code _have the same_ dependency, they in turn
are coupled to each other!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without proper dependency injection container this is way too hard to achieve. FreeCAD also very much relies on singletons. Is it a good thing? Of course not. This is however not something that can be changed easily.

This rule should be rewritten to "Do not create new singleton classes" as this is the most that we can expect from new code at this stage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Handbook probably deserves something like history tour. Codebase is ancient and effort is being make to modernise it. Consequence is that something what already is in the codebase is no excuse for new code sneaking in.
On practical level it depends on maintainers - this is where code enters repository. So those with commit rights need to find consensus, I'm just a random person watching new code flowing and finding it very inconsistent across different commiters. PR was opened in hope this chapter ends as something people agree upon when merging code and I still think such a discussion belongs to forum, but this is in direct contradiction what moderator thinks.

Comment on lines +222 to +241
## design 👍

> Any fool can write code that a computer can understand. Good programmers
write code that humans can understand
--- Martin Fowler

Something well-designed is _instantly_ understandable, and a pleasure to work
with. Programming principles developed over the last 50 years ensure
well-designed code not only runs well, but is also understandable by humans.

Well-designed code is adaptable, flexible, easily changed to suite new
circumstances.

Well-designed code is completely free from hard-coded data.

Understandable code can be more easily evaluated for correctness.

For a novice programmer many of these concepts are probably incomprehensible,
but with a little study and a determination to understand established
principles, better code will ensue.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is another manifesto - it does not bring any concrete help for newcomers.

Comment on lines +260 to +263
## inappropriate type 👎

When everything is a string it is diffcult to understand what’s what.
Apply suitable abstraction.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wording is extremely unhelpful, this should explain on examples and bring concrete rules like "Try to use the most specific type as you can" / "Do not use too generic types unless you really mean it".


Avoid default constructors. If there is not yet a value for an object,
then don’t create it! Declare variables where they are used (not at
the start of the block like last century C). Joining declaration and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not at the start of the block like last century C).

Do we really need such remarks?

Comment on lines +361 to +377
> One of the most popular features of Modern C++.
--- Jonathan Boccara, 2021

A lambda (since C++11) is like a function that can be named, passed into and
out of functions. They accept parameters, can be generic (since C++ 14), do a
really good job of type deduction (auto), can be constexpr (since C++17).

Lambdas can capture data from enclosing scopes.

Lambdas enforce encapsulation, simplifying surrounding scopes.

Lambdas are indispensable when breaking up complex code into individual
responsibilities, perhaps as a precursor to moving to functions. Ditto
when removing repetition.

Whilst lambdas are quite happy with simple auto parameters, best not
to omit const or & as appropriate. Types may be required to help the IDE.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lacks a good examples where lambdas are useful, and when they should be avoided.

```cpp
auto [name, value] = func("qty", 2);
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also use remark about std::optional

auto [name, value] = func("qty", 2);
```

## repetition 👎
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should note that not every code repetition is bad. Repetition of BUSINESS LOGIC is bad but sometimes it is better to have duplicated code than to couple two unrelated things together simply because their code looks similar.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess most people got that by themselves... I will not answer every review comment above individually as these are revolving around lack of examples. Those would need to be artificially created as taking them from code base is no-go. To me that is not worth doing so.
Also I cannot alter this particular commit as I'm not the author. And author is not allowed to use github and his post are being deleted from forum. This was in fact the reason beyond creating this PR. We can continue after deciding, whenever it is okay to use code examples from FreeCAD. Every change agreed upon will be then introduced as additional commit against this one.

@bofdahof
Copy link

I don't understand what all the fuss is about. At only 10 pp long the document is, for my money, worth supporting. Why make this all so complicated? People can decide for themselves if it is useful.

This discussion suggests the document has aspirations to be somehow "official". There is no evidence of that, so what is all the fuss is about? It should rather be treated as a useful adjunct. Good medicine.

Proposed practices are not established in FreeCAD code base

I don't see the document suggesting "proposed practises". Looks to me more of a terse primer, a summary of relevant information that invites user research. Just an added reference work.

I think it is completely wrong to suggest FreeCAD codebase is a worthwhile benchmark! There is whole wide world outside of FreeCAD!

lack concrete examples and sometimes even good explanation why it is the better way

I don't think it pretends to be a programming 101 lecture course.

We should be welcoming for people and language used here is very much intimidating

Huh? Looks to me exactly the language of 21st century C++ programming. Surely nothing for grownups to be scared of!! A lot of the stuff in there is not even C++ specific, just good old-fashioned basic practises for any language. Are you proposing lowest common denominator sets the ceiling on ambition?

I'm honestly not convinced that the find_if here is more readable

Then perhaps you might benefit from a bit of study of std library algorithms, what they are, and why they exist, and why the message nowadays is to not use raw loops. Look it up! There's miles of YT videos, web stuff. It is not secret or esoteric.

Simple code is simpler to maintain

Simple code can be part of a complicated mess and become extremely difficult to maintain. Its not just the few lines of code ...

This lacks explanation why we should make things const and why immutability is important

Again, if that is not understood then perhaps some broader study would be helpful. Immutability is basic stuff, in any language, even if in FreeCAD it is largely ignored. Note: US government is mandating C and C++ to be too unsafe for future government work.

sometimes the clang-format makes the code to look worse and less readable than before

I find after removing repetition and other evils like breaking Law of Demeter, code gets smaller and simpler. Except for tables of data (which benefit from formatting being turned off), the code then formats beautifully. All the more reason for code and data to be kept separate, as the document espouses. Codified data is just horrible.

Without proper dependency injection container this is way too hard to achieve

Misses the whole point! The best DI container won't fix spaghetti code. Dependency injection is impossible until buried dependencies (eg singleton access eg Application->getInstance()) are brought out to the constructor. Then you have to think about the various methods of handling interfaces etc. If it wasn't done when the code was new, it is a nightmare to do later. When code is refactored to remove dependencies, everything gets simpler, better.

From the forum: https://www.youtube.com/watch?v=kCYo2gJ3Y38

In C++ DI is particularly challenging, compared to many other languages, due in no small part to not having reflection. But that is no excuse for not making moves to approach it manually. DI containers have several problems themselves: They add a global, and they reduce performance (which for C++ programmers is often the holy grail).

This lacks a good examples where lambdas are useful, and when they should be avoided.

Honestly, this freaks me out! There is almost nothing to hate about lambdas. I remember in the document it quotes them as being one of the most popular features of modern C++. There are few in FreeCAD. But there are absolutely tons of opportunities to use them, particularly in refactoring (but there doesn't tend to be much refactoring in FreeCAD - probably cause it's hard)! The document IMO does a good job outlining their usefulness.

This requires examples of when enums are good, when enums are bad

It's right there a few lines down:

**BUT! Enums that codify _data values_ are a significant code smell!**

See also algorithms + data structures = programs.

This could also use remark about std::optional

I'm sure there are a zillion things that could go in there. This is 10pp, as 3x380V said aimed squarely at observations about FreeCAD codebase. Core Guidelines is something like 500pp!

This should note that not every code repetition is bad

There are quotes in the document from seriously important C++ folk who strongly disagree! Repetition IS evil! Copy-Paste is for script kiddies, not serious programmers like us, right?

That's enough!

@bofdahof
Copy link

I don't want developers (or maintainers) to have to consider, or argue about, code formatting

Remember the furious arguments against uniform code formatting? It got there in the end.
Unit testing seems to be going through similar battles right now. Hopefully that will die down soon too.
And now there is a new battle for code quality. ...

@chennes
Copy link
Member

chennes commented Nov 13, 2024

Let's not characterize any of these discussions as "battles" -- there are no enemies here, only a group of people who want to improve FreeCAD. We are having a reasonable, professional discussion about how best to approach that task given the constraints of the project. A few comments above stray awfully close to ad hominem territory: please keep the Code of Conduct in mind as you participate here.

@bofdahof
Copy link

"battle" is a very common figure of speech applied to the sway of the pendulum in a healthy debate such as this. Surely a stretch to suggest even remotely ad hominem ...

@bofdahof
Copy link

I note the document has 23 sections, which is quite a lot, and is naturally blurring the discussion.
To narrow the focus a little, IMO the most critical yet least understood are the concepts embodied in:

  • algorithms + data structures = programs
  • dependencies
  • naming
  • repetition
  • variable sets

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants