Have you ever come across this code?
process(true, false);
This function, judging by the name, processes something (process). But what do the parameters mean? Which parameter is true and which is false? This code cannot be judged by the calling code.
We will have to look into the function declaration, which gives a hint:
')
void process(bool withValidation, bool withNewEngine);
Obviously, the author uses two parameters of bool type as
flags (toggles). The implementation of the function may look like this:
void process(bool withValidation, bool withNewEngine) { if (withValidation)
The purpose of the flags is obvious, since each of them has a meaningful name. The problem occurs in the calling code.
And it's not just that we cannot immediately understand which flags are used. Even knowing this, we can easily confuse their order. In fact, my first example should look like this:
process(false, true);
But I confused the order of the arguments.
Faced with this bug, the programmer is likely to add comments to the function call to explicitly show his intentions:
process( false, true);
And this is too similar to the named parameters of a function — a feature not found in C ++. If she was, she could look something like this:
But even if it were in C ++, it would hardly be compatible with direct forwarding (perfect forwarding):
std::function<void(bool, bool)> callback = &process; callback(???, ???);
This can be associated with an even more insidious bug that is much harder to track down. Imagine that the process function is a virtual class method. And in some other class we redefine it, while placing the flags in the wrong order:
struct Base { virtual void process(bool withValidation, bool withNewEngine); }; struct Derived : Base { void process(bool withNewEngine, bool withValidation) override; };
The compiler will not notice the problem, since the parameters differ only by name, and their types are the same (both bool).
The bugs arising from the use of logical parameters in the interface do not end there. Due to the fact that almost all built-in types are converted to bool, the following example compiles without errors, but does not what is expected:
std::vector<int> vec; process(vec.data(), vec.size());
A more common problem is using bool in constructors. Let there is a class with two constructors:
class C { explicit C(bool withDefaults, bool withChecks); explicit C(int* array, size_t size); };
At some point you decide to remove the second constructor, and it may be hoped that the compiler will point you to all the places that need fixing. But this is not happening. Due to implicit conversions in bool, the first constructor will be used wherever the second one was used.
However, there is a reason why people usually use bool to represent flags. This is the only built-in type available out of the box and intended to represent only two possible values.
Transfers
To solve these problems, we must have a type other than bool that would satisfy the following requirements:
- a unique type is created for each flag,
- implicit conversions are prohibited.
C ++ 11 introduces the concept of enumeration classes that satisfy both requirements. We can also use the bool type as the base type of the enumeration; thus, we guarantee that the enumeration contains only two possible values ​​and has the size of one bool. First we define the flag classes:
enum class WithValidation : bool { False, True }; enum class WithNewEngine : bool { False, True };
Now we can declare our function:
void process(WithValidation withValidation, WithNewEngine withNewEngine);
There is some redundancy in this declaration, but the order of using the function is now what it needs:
process(WithValidation::False, WithNewEngine::True);
And if I put the flags in the wrong order, I get a compilation error due to the type mismatch:
process(WithNewEngine::True, WithValidation::False);
Each flag has a unique type that works properly during forwarding (perfect forwarding), and you can’t put the parameters in the wrong order in the function declarations and virtual method overrides.
But using transfers as flags has its price. Flags are somewhat similar to bool values, but enumeration classes do not imitate this similarity. Implicit conversions to bool and back do not work (and this is good), but explicit conversions do not work either, and this is a problem. If we look again at the body of the process function, we will understand that it does not compile:
void process(WithValidation withValidation, WithNewEngine withNewEngine) { if (withValidation)
I have to use an explicit conversion:
if (bool(withValidation))
And if I need a logical expression with two flags, it will look even more inadequate:
if (bool(withNewEngine) || bool(withValidation)) validate();
In addition, for an instance of an enumeration class, you cannot do direct initialization from bool:
bool read_bool(); class X { WithNewEngine _withNewEngine; public: X() : _withNewEngine(read_bool())
Again you have to do an explicit conversion:
class X { WithNewEngine _withNewEngine; public: X() : _withNewEngine(WithNewEngine(read_bool()))
This can be considered an additional guarantee of security, but there are too many explicit conversions. Enumeration classes have more “explicit” than constructors and transformation operators declared as “explicit”.
tagged_bool
Due to problems with using bool and enumeration classes, I had to make my own tool called tagged_bool. You can find its implementation
here . She is quite small. With its help, flag classes are declared like this:
using WithValidation = tagged_bool<class WithValidation_tag>; using WithNewEngine = tagged_bool<class WithNewEngine_tag>;
You will have to pre-declare a class tagname, such as WithValidation_tag. There is no need to write a definition for it. It is used to create a unique tagging_bool class template specialization. This specialization can be explicitly converted to bool and back, as well as to other specializations of the tagged_bool template, since, as is usually the case in practice, some bool passed to the lower levels of the application will later become another flag with a different name. You can use the flags created in this way like this:
void process(WithValidation withValidation, WithNewEngine withNewEngine) { if (withNewEngine || withValidation)
That's all. The tagged_bool is part of the
Explicit library , which contains several tools that allow you to more explicitly express your intentions when designing interfaces.
From translator
Andrzej previously had another article about tags - “
Intuitive interface - Part I ” dated July 5, 2013 (Part 2 never came to light, do not search). In short, there was such a problem:
std::vector<int> v1{5, 6};
When the behavior of a constructor depends on the form of parentheses, this in itself is dangerous. In addition, it makes the calling code incomprehensible:
std::vector<int> v(5, 6);
What are 5 and 6? Will it be 5 sixes or 6 fives? If you forgot - go to see the documentation.
And I would like to have another constructor that creates an empty vector with a given capacity: std :: vector v (100). Unfortunately, the constructor that accepts one size_t is already taken — it creates a vector with the given size, filled with default-constructed objects.
Andrzej mentions that such an order of things does not make it possible to take full advantage of the possibilities of the direct transmission, but explained in the comments that this problem is solved without any tags.
Andrzej came to the conclusion that the implementation of the vector in the STL library is not entirely successful. It would be much easier if tags were used in its constructors:
std::vector<int> v1(std::with_size, 10, std::with_value, 6);
In relation to this article, it would look like this:
process(withValidation, true, withNewEngine, false);
The difference is that the tag and value are now merged into one object. In the article “
Competing constructors ” dated July 29, 2016, Andrzej wrote in passing that he did not like the idea of ​​such a union.
vector<int> w {with_size{4}, 2};
Now it is not tags, but full-fledged objects. Someone might think of putting them in a container:
vector<with_size> w {with_size{4}, with_size{2}};
The behavior of this code again depends on the form of the brackets. What kind of joy was introducing tags if we returned to the same problem again? At least, simple tags are unlikely to anyone want to store in a container. After all, they can have only one value.
With bool, however, this threat is not so terrible. STL container constructors do not accept bool, except as part of initializer_list'ov. Apparently, this is why Andrzej decided this time to merge the tag and meaning.
Finally, I will cite the translation of several comments on the article.
Comments
kszatan
February 17, 2017 at 11:36 am
I would first of all think about getting rid of all these flags and bring the code for validations and the new / old engine (new / old engine) to separate classes in order to pass them as arguments. The "process" function is already doing too much.
Andrzej Krzemieński
February 17, 2017 at 12:03 pm
In simple cases, dropping any flags may indeed be the best choice. But when the decision to set a flag is made several levels higher in the call stack, such refactoring may not be feasible or impractical.
int main() { Revert revert_to_old_functionality {read_from_config()}; Layer1::fun(revert_to_old_functionality) } void Layer1::fun(Revert revert) {
=== End of thread ===
micleowen
February 17, 2017 at 10:41 pm
class C { explicit C(bool withDefaults, bool withChecks); explicit C(int* array, size_t size); };
“Explicit” is used in constructors with one parameter.
Andrzej Krzemieński
February 20, 2017 at 8:22 am
There is a solid reason to declare almost all constructors as “explicit”, and not just constructors with 1 argument (especially with the release of the C ++ 11 standard). Sometimes even the default constructor is better to be declared as “explicit”. For those interested, I advise you to refer
to this article .
=== End of thread ===
ARNAUD
February 18, 2017 at 6:39 pm
“Implicit conversions to bool and back do not work (and this is good), but explicit conversions do not work either, and this is a problem.”
I do not understand what's wrong with that:
if(withValidation == WithValidation::True)
First, you use the well-known possibility of the language, and your code is perfectly readable and understood by all C ++ specialists. Then you go to use a special template to automatically convert to bool and back? It does not convince me.
And further. Imagine that after some time one of the parameters will cease to be bool and will be able to take the values ​​no_engine, engine_v1, engine_v2 ... The enumeration class allows you to make such an extension in a natural way, unlike your tagged_bool.
Andrzej Krzemieński
February 20, 2017 at 8:36 am
You raised two questions.
1. Choose between
if (withValidation || withNewEngine)
and
if (withValidation == WithValidation::True || withNewEngine == WithNewEngine::True)
And, in the case of using namespaces:
if (withValidation == SomeNamespace::WithValidation::True || withNewEngine == SomeNamespace::WithNewEngine::True)
For me, this is a trade-off between the desired level of security and usability. My personal choice is something safer than bool, but not as wordy as enumeration classes. Apparently, your compromise is closer to the classes of transfers.
2. The ability to add a third state
If you foresee that you may need a third state in the future, then enumeration classes may be preferable. And may not be. Because when you add the third state, all your ifs continue to compile properly, although you may want to edit them to add a third state check.
In my experience, these flags are used as temporary solutions, and their further development is not to add a third state, but to get rid of two existing ones. For example, I improve some part of the program, but for a couple of months I want to give users the opportunity to switch back to the old implementation, in case I overlooked something, and improving will only ruin everything. If after a couple of months all users are satisfied, I remove the support of the old implementation and get rid of the flag.
=== End of thread ===
mftdev00
March 13, 2017 at 1:05 pm
I don't like flags at all. They contradict the principle of sole responsibility. Do something, if true, do something else, if false ...
Andrzej Krzemieński
March 13, 2017 at 1:10 pm
I agree. Wherever possible, you need to do without flags.
=== End of thread ===
Sebb
March 21, 2017 at 6:09 pm
Is it possible instead of explicitly removing constructors for each type:
constexpr explicit tagged_bool (bool v) : value {v} {} constexpr explicit tagged_bool (int) = delete; constexpr explicit tagged_bool (double) = delete; constexpr explicit tagged_bool (void*) = delete;
... just delete them for all types (except bool) at once?
constexpr explicit tagged_bool (bool v) : value {v} {} template <typename SomethingOtherThanBool> constexpr explicit tagged_bool(SomethingOtherThanBool) = delete;
Andrzej Krzemieński
March 22, 2017 at 7:32 am
I just did not consider this possibility when I developed the interface. Maybe it would be useful to add it. But now, when you suggested it, I see one case where it would have a negative effect: someone can use their own (safe) boolean type with an implicit conversion to bool. In this case, we may need to allow this type to work with tagged_bool.