Download presentation
Presentation is loading. Please wait.
Published byMarjorie Pitts Modified over 6 years ago
1
C++ coding standard suggestion… Separate reasoning from action, in every block.
Hi, this talk is to suggest a rule (or guideline) to simplify C++ code – separate reasoning from action, in every block. I don’t see people doing this, probably because the result goes against our instincts of what good code looks like, but I want to argue its good points. I hope that thinking about the consequences of the rule will be useful, and I welcome feedback. In 20 years working as a programmer, the most serious problem I see, which occurs in every codebase I’ve worked with, is that code gets too complicated over time. There are many aspects to this, but I’m going to talk about how small amounts of code can get complicated. Lets take the example of a class member function that calculates fibonacci numbers. Jeff Hannan
2
Fibonacci v.1 The function calculates the nth fibonacci number.
Even if the function is written in a really clear way to start with,
3
Fibonacci v.2 Once it is modified with bug fixes
4
Fibonacci v.3 and additional features, it will become more complex.
5
Fibonacci v.4 I’ve added a lot to the function here just to gather statistics, and the result is not good. A function with lots of changes tends to become crammed with logic, making it unwieldy and difficult to understand. This member function now has several outputs, and they are written to throughout the function. Refactoring is a good way to deal with this. But the reality is that for many of us, despite best efforts, code degenerates over time. I work with great programmers, so it is not due to bad coding. We just can’t refactor fast enough. However, coding standards have to be followed for all new code, so they are an effective way to keep code quality high, in the face of changes.
6
Coding standards that reduce complexity have a big impact.
So, coding standards that reduce complexity have a big impact.
7
The rule reasoning statements action statements
Divide up the statements in a block into 2 parts, in the following order: reasoning statements how the output is calculated action statements what the program is doing The rule then, is one that can be applied at all times, to most types of function. It is to divide up the statements in a block into 2 parts, in the following order: reasoning statements action statements The reasoning statements do not write any outputs of the block. It is the const part of the block. Data gathering and working out, all within block scope. The action statements just write the outputs of the block. It simplifies because it divides up the code into two parts, each of which you only look at for a different reason. To understand what the program is doing, we only need to look at the action - the output that is written. To understand how the output is calculated, we only need to look at the reasoning.
8
If you look at some code, I expect you will find that these expressions and statements are all intermingled. A typical example is if (newX != oldX) as in { ProcessNewX(newX); } That one line contains 2 pieces of logic, that can be separated. const bool xHasChanged = ( newX != oldX ); if (xHasChanged) I know that many programmers will look at that, and think it is unnecessary overhead. How is this simplifying things, you rightly ask? Well, the line is simpler than the line The reasoning for how x has changed has been moved up. If we are just looking at what the program is doing, we don’t need to know how xHasChanged is calculated. Of course we could do this: if (HasXChanged(newX)) and I accept that is pretty clear, but it still isn’t as simple as checking a boolean. This approach will lead to an increase in temporary variables. And too many temporaries are seen as a bad thing, because there are more variables to think about. But though temporary variables increase the number of variables in scope, they do allow us to discard used data. However much information is used to determine if x has changed, it is boiled down to a boolean. When it comes to the Action, you can mentally discard the reasoning.
9
Temporary variables can reduce the number of inputs into the Action code.
Temporary variables can therefore reduce the number of inputs into the Action code.
10
Fibonacci v.4 inputs and outputs
Function inputs: n Function outputs: m_numberOfCalls m_lastResult m_totalSum next total Loop block inputs: n first second total m_totalSum Loop block outputs: next else block inputs: n else block outputs: total m_totalSum next One of the great causes of complexity is having too many inputs and outputs. A function transforms inputs into outputs. But when there are several, this becomes a very complicated tranformation, made worse when there are temporary variables, and access to member variables and object data. A block has access to the variables in its outer scope. So, a small piece of code can have access to a large amount of data. This can be seen in the example function. The function itself has 1 input and 5 outputs. The else block has a smaller number of outputs, but the loop block has 5 outputs, some of which are also function outputs. This code is getting complicated, and means that someone reading the code has to unpick it. Especially if they have to fix a bug or make further modifications. When I see code that has many inputs and outputs, I wonder if the authors understood the problem space, and the mapping that is being performed. When code has been added to over time, like this one, probably not. Sometimes, I’m the author. Without refactoring into functions, we can’t control all the potential inputs and outputs. But, we could have written the code in such a way that minimises the number of inputs and outputs at each stage.
11
Block input/output pipeline
Block Inputs Block Inputs Subset of data accessible to block Don’t write data that is outside block scope Reasoning code Block code Calculate block outputs Action Inputs Minimise these Make this as simple as practically possible Action code Block Outputs Subset of data accessible to block and/or function return value Consider that each block is an input/output pipeline. The block code uses a subset of all the data it has access to, and transforms the block inputs into the outputs. By separating the block code into Reasoning and Action, we can say that the Reasoning code does some calculation and produces output. But must write no data that is outside block scope. The Action code, takes the minimal subset of inputs. We should make this as simple as is practically possible. Moving anything that can be pre-calculated up into the Reasoning code. The action code is what changes the data, and it should be easy to see how that happens. I anticipate that a big question is – can you really divide up the logic of a block like that? Well, I’ve looked at a lot of code and found that typically you can, but not always. There are varying degrees at which you can apply the idea, and I’m trying to take it as far as I can. Block Outputs
12
Fibonacci v.4 using rule Here is how the function is written following the rule. The result is long-winded. It goes against my instincts, by using unnecessary temporary variables, and bloating the functions. But it is much easier to see, what each block does. All the output is written at the end of the function, so it is easy to see what the function does. Each of the 3 blocks in the if statement ends by writing out the same 3 variables. And the loop updates and writes out 4 variables. I’ve commented the Reasoning and Action parts. Essentially, each block is self contained, and like a function in itself. So, a function with nested blocks can be understood more easily, because the outputs of any block should be obvious. And it should be easy to see where the outputs of the function are set. My conclusion is that in practice, the rule is beneficial most of the time. In this example, the Action only writes output directly. But, the Action can call other functions, use if statements and loops, as long as those write the outputs of the block.
13
Summary Code is simpler Coding standard to resist complexity
Not refactoring, but helps refactoring Oversimplifies deliberately I find that this rule makes it easier to read and understand code. That code is simpler when you split the reasoning (or working out) from the action. And by making this a coding standard, it is one way to resist code becoming too complex. This rule is for writing code, so it is not refactoring. But it does make refactoring easier, because each block is already organised like a function, with explicit outputs. And, while this isn’t an ideal way of writing code, because it is oversimplifying it, it prepares the code for future changes. I will finish with the full rule, as I apply it. I hope you’ll give it some thought, thank you.
14
The rule as I use it The rule applies hierarchically, to the statements in each block, like this: int FunctionName() { // Reasoning // Action } Reasoning - can contain sub-blocks and call functions. Defining and setting variables with block scope is done here. Action - can contain sub-blocks and call functions, but only if they write the outputs of the block. i.e. you can pass a reference to a function which writes to it. Set outputs, or update outputs using an operator e.g. m_totalSum += totalSumDelta. Asserts can be anywhere and are not covered by the rule.
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.