In development, there are many situations where you need to express something with the help of an " optional
" object, which may or may not contain any value. You can implement an optional type with several options, but with C ++ 17 you can accomplish this with the most convenient option: std :: optional.
Today, I have prepared one refactoring task for you, to which you can learn how to apply the new C ++ feature 17.
Let's quickly dive into the code.
Imagine that there is a function that takes an ObjSelection
object, which is, for example, the current position of the mouse pointer. The function scans the selection and finds the number of animated objects, whether there are civilian units there and whether there are military units there.
The existing code looks like this:
class ObjSelection { public: bool IsValid() const { return true; } // more code... }; bool CheckSelectionVer1(const ObjSelection &objList, bool *pOutAnyCivilUnits, bool *pOutAnyCombatUnits, int *pOutNumAnimating);
As you can see above, the function mainly contains output parameters (in the form of raw pointers) and returns true/false
to indicate the success of its execution (for example, the selection may be incorrect).
I will skip the implementation of this function, but below you can see the code that calls this function:
ObjSelection sel; bool anyCivilUnits { false }; bool anyCombatUnits {false}; int numAnimating { 0 }; if (CheckSelectionVer1(sel, &anyCivilUnits, &anyCombatUnits, &numAnimating)) { // ... }
Why is this feature not perfect?
There are several reasons for this:
Anything else?
How will you refactor it?
Guided by Core Guidelines and new features of C ++ 17, I plan to divide the refactoring into the following steps:
std::tuple
, which will be the return value.std::tuple
to a separate structure and reducing std::tuple
to std::pair
.std::optional
to emphasize possible errors.This article is part of my C ++ 17 library utility series. Here is a list of other topics that I’m talking about:
std::optional
.std::optional
.std::variant
.std::any
.std::optional
, std::variant
and std::any
.std::string_view
.std::filesystem
.C ++ 17 STL Resources:
OK, now let's refactor something.
std::tuple
The first step is to convert the output parameters to std::tuple
and return it from the function.
In accordance with F.21: For returning multiple output values, it is preferable to use tuples or structures (English language)
The return value is documented itself as a "return only" value. Note that a function in C ++ can have multiple return values using the tuple usage agreement (including the (std::pair
)std::pair
, with the additional use of (possibly)std::tie
on the caller.
After the change, our code should look like this:
std::tuple<bool, bool, bool, int> CheckSelectionVer2(const ObjSelection &objList) { if (!objList.IsValid()) return {false, false, false, 0}; // local variables: int numCivilUnits = 0; int numCombat = 0; int numAnimating = 0; // scan... return {true, numCivilUnits > 0, numCombat > 0, numAnimating }; }
A bit better, isn't it?
Moreover, you can use structured bindings (English language: Structured Bindings, note lane.: There is no well-established name in Russian ) to wrap the tuple on the caller:
auto [ok, anyCivil, anyCombat, numAnim] = CheckSelectionVer2(sel); if (ok) { // ... }
Unfortunately, it seems to me that this is not the best option. I think it's easy to forget the order of the output variables in a tuple. There is an article on SimplifyC ++ on this topic: std::pair
and std::tuple
(English language) .
Moreover, there is still the problem of expanding the function in the future. Therefore, when you want to add another output value, you will need to expand the tuple on the caller.
Therefore, I propose the next step: structure (the same is proposed in Core Guidelines).
The output results are related data. Therefore, it seems like a good idea to wrap them in a structure called SelectionData
:
struct SelectionData { bool anyCivilUnits { false }; bool anyCombatUnits { false }; int numAnimating { 0 }; };
After that we can rewrite our function as follows:
std::pair<bool, SelectionData> CheckSelectionVer3(const ObjSelection &objList) { SelectionData out; if (!objList.IsValid()) return {false, out}; // scan... return {true, out}; }
And on the caller:
if (auto [ok, selData] = CheckSelectionVer3(sel); ok) { // ... }
I used std::pair
, so we still save the flag of successful function execution, it does not become part of the new structure.
The main advantage is that we got a logical structure and extensibility here. If you want to add a new parameter, simply expand the structure.
But std::pair<bool, MyType>
very similar to std::optional
, isn't it?
std::optional
Below is a description of the type std::optional
with CppReference :
The template classstd::optional
controls an optional value, i.e., a value that may or may not be represented.
A common example of using an optional data type is the return value of a function, which may return an erroneous result during execution. Unlike other approaches, such asstd::pair<T, bool>
, the optional data type is well managed with heavy objects for construction and is more readable, since it clearly expresses the intentions of the developer.
This seems to be the perfect choice for our code. We can remove ok
from our code and rely on the semantics of the optional type.
For reference, std::optional
was added in C ++ 17, but before C ++ 17 you could use boost::optional
, since they are almost identical.
The new version of our code looks like this:
std::optional<SelectionData> CheckSelection(const ObjSelection &objList) { if (!objList.IsValid()) return { }; SelectionData out; // scan... return {out}; }
and on the caller:
if (auto ret = CheckSelection(sel); ret.has_value()) { // access via *ret or even ret-> // ret->numAnimating }
The version with the optional data type has the following advantages:
T
It seems to me that the version using the optional type is the best in the considered example.
You can play with the code at this link .
In this article, you saw how to refactor a lot of badly smelling code with output parameters using an optional type. The wrapper over the data in the form of an optional type makes it clear that the calculated value may not exist. I also showed how to wrap several function parameters in a separate structure. You can easily extend your code using separate data types while maintaining the logical structure of the code.
On the other hand, this new implementation omits an important aspect of the code: error handling. At the moment, you can not find out why the function could not calculate the value. In the previous example, when implemented with std::pair
, we could return some kind of error code to indicate the reason.
Here is what I found in the documentation boost (English language) :
The optional data typestd::optional<T>
recommended to be used in cases where there is only one reason why we could not get an object of typeT
and where the absence of a value ofT
just as normal as its presence.
In other words, the version of std::optional
looks great only if we take the situation of "incorrect allocation" for the usual working situation in the application ... this is a good topic for the next article :) I wonder what you think about the places where would be great to use std::optional
.
How would you refactor the first version of the code?
Would you return tuples or create structures from them?
See the following article: std::optional
.
Below you can see some articles that helped me with this post:
Source: https://habr.com/ru/post/369811/
All Articles