Logical constness
-
@kshegunov said in How do I get QTemporaryFile and QtConcurrent::run to play nicely?:
@Vadi2 said in How do I get QTemporaryFile and QtConcurrent::run to play nicely?:
I didn't realise const function was not enforced by the compiler!
It is enforced, but in the example the member's declared as
mutable
, which I find pretty abominable to begin with.mutable
basically means that the specific member can be modified from anywhere as it is "not part of the class interface". It's equivalent to stripping down the const manually, e.g.:class SomeClass { public: void someMethod() const { SomeType & playingWithFireHere = const_cast<SomeType &>(member) } private: SomeType member; };
I recently read this: https://isocpp.org/wiki/faq/const-correctness#logical-vs-physical-const and it kind of revolutionized my thinking. I'm not a C++ expert but a wannabe. I once tried to use const in real codebase but because const is viral and a library didn't use const it was horrible and I gave up. Now I'm trying to learn how to use const correctly and as much as possible. Some thoughts:
- "
mutable
basically means that the specific member can be modified from anywhere as it is "not part of the class interface". It's equivalent to stripping down the const manually"
I don't agree completely. Mutable should mean that the member isn't part of the logical state, not that it's not part of the class interface. It's not equivalent to stripping down the const manually, even though const casting can be used for the same effect and is equivalent for its effects in specific situations. It should mean the mutable member is an implementation detail whose existence or state can't be seen from outside in any way through the public interface and therefore can be changed even in const functions. It has not only technical meaning but also design meaning just like public, protected or private, unlike casting const away.
- (See the original thread about const with threaded code.) Const has almost nothing to do with thread safety.
What const promises, as far as I have understood, is that the function itself leaves the logical state of the object intact, i.e. it's the same before entering a const function and after leaving it, as much as the function in question handles it. It can't guarantee for example that a data member which is explicitly shared with another object using a pointer doesn't change while running the function code. There sure are tons of possibilities how const can't guarantee thread safety even without mutable or const cast. You can't use const to deduce about thread safety, it's not meant for that at all. Designing thread-safe code is extremely difficult and unfortunately using const doesn't help much in that (although it probably can help somewhat). If the C++ FAQ is correct - and anyone would have hard time arguing with it - const shouldn't be used to guarantee thread-safety and when seen in code it shouldn't be interpreted as any guarantee about thread-safety.
- Coding with logical state in mind is much more difficult than thinking just about "changing any member data".
However, if I can learn to do it, it will do good to my design and code. Using mutable is bad if I don't understand the design decisions and philosophy behind it. If I understand desing-from-outside etc. I can use mutable correctly.
- With my experience It's difficult to tell what actually is part of the logical state.
For example, how much an external resource which is used inside the object is part of the logical state. What if the object is an interface to read-write data within a file? What if it's the only interface to that? What if there are other interfaces to the same file? What about asynchronous signal-slot mechanism with for example a network? Has the logical state changed if I initiate a network action and the object is waiting for an asynchronous answer but doesn't use any data marking its state? I would like to have a tutorial to logical state and constness.
- "
-
- "not part of the class interface" == isn't part of the logical state. As the link you posted says:
The constness of a method must make sense to the object’s users
on "It's equivalent to stripping down the const manually", it basically is. for example,
QBoxLayout::sizeHint() const
usesconst_cast<QBoxLayout*>(this)->d_func()->setupGeom();
as alternative to a mutabled_ptr
- nobody ever said the opposite, in the forum post above we actually argue that, for example, it's never thread safe to execute a non static method (const or non const) from a thread different from the one owning the object
3/4) even if you do, someone else might not. the code should be easily maintainable so unless you absolutely positively need mutable, avoid it
P.S.
Summoning old @kshegunov here as he might contradict everything I said and if he does, disregard my version and go with his -
@VRonin said in Logical constness:
as he might contradict
He mightn't. :)
Here goes the long (, long) story:
@Eeli-K said in Logical constness:
Now I'm trying to learn how to use const correctly and as much as possible.
The one-line rule is: everything that's humanly possible should be const.
It allows for better safety and smoother use and integration into other code. You don't useconst
whenever you can't (i.e. the thing is not immutable, like in setters, methods that change the object).Mutable should mean that the member isn't part of the logical state, not that it's not part of the class interface.
Those are one and the same. The interface of a class is what is exposed to the user of said class, so it overlaps 100% with your definition of "logical state".
It's not equivalent to stripping down the const manually, even though const casting can be used for the same effect and is equivalent for its effects in specific situations.
Forget the syntax, think semantics (i.e. meaning), you're trying to argue that division by zero is not the same for
int
andshort
, while technically that's true as the two types are syntactically (and assembly-wise) different it's of no consequence to the meaning of the operation, it's undefined all the same - it has no meaning. So whether you usemutable
for some member or you strip the const for that member wherever you decide to modify it, again, same meaning - you're modifying a member that's otherwise immutable, same meaning - same consequences, it just doesn't look as ugly from afar.It should mean the
mutable
member is an implementation detail whose existence or state can't be seen from outside in any way through the public interface and therefore can be changed even in const functions.Exactly as with const-casting. You can't see that from outside the class, you're oblivious of it's existence, even more so than
mutable
which at least (most often) you can see in the class declaration.Const has almost nothing to do with thread safety.
That is true, and not true. It is true in the broadest sense that a
const
method can break reentrancy and/or tread-safety. However, in practice, such code often makes for a pile of s*it to use. While technically you indeed shouldn't depend on the const method calls to be safe between different threads, it makes a lot of sense to require it. I mean is that you require the author of that class to make everything that's possible to provide you with an interface without side effects, so you can use it effectively. I'll throw in an example, so this doesn't sound that far-fetched:Suppose you have the task to calculate some heavy stuff but it submits easily to preprocessing and reusing of precalculated results. Then suppose someone decided it's a great thing to add some internal cache optimization for
QHash
(or the STL's hash table, doesn't really matter) and to update a cache frequency counter whenever you fetch a record from said hash table. That internal caching isn't exposed anywhere, so it does fall under the definition of "physical state" (as per the article above). Now that may sounds perfect in theory, you get some automatic caching and everything's fine. Well the real world disagrees, because the fetching routine is no longer thread-safe. Since it's no longer thread safe, you have to sync the accesses through a mutex, and suddenly your simple task of preprocessing a number of heavy calculations putting them in a nice tidy hash and concurrently (and safely) fetching the results becomes an elephant of a problem.- Because you have to manually synchronize the reading threads (as they're not reading-only anymore).
- And more importantly, you lose the ability to independently look up hashed records without locking, thus your multi-threading efficiency drops considerably.
So, for all intents and purposes you should not use
mutable
unless there is no other way, and I mean there is really not. (does not so rigidly apply for classes you use internally, where you can do whatever you want, since you're the only user).Coding with logical state in mind is much more difficult than thinking just about "changing any member data".
Yes, because in a perfect world the logical and the physical state would be 100% the same. So the rule of thumb is, try to implement them as such, don't add superfluous members and functionalities in your classes and you'll have very little problems. Don't try to solve it all, leave the possible optimization problems to the users, as they will be better suited to deal with them having all the usage information at hand. If you still decide to do some kind of internal optimization and possibly use mutable in a class that's available to users, document that with whale-heavy text.
With my experience It's difficult to tell what actually is part of the logical state.
Everything that's exposed through the class interface - be it setters, getters, signals emitted also fall in that category (the members are exposed through the dependent setter, getter or signal). Basically everything that's communicated from the class to the outside world is part of its interface directly or indirectly.
[Edit: Rephrased the ambiguous line, thanks to @VRonin for pointing it out]
-
@kshegunov said in Logical constness:
He mightn't. :)
I just got lucky for once
@kshegunov said in Logical constness:
While technically you indeed shouldn't depend on the method to be thread-safe when declared as immutable, it makes a lot of sense to require it.
I don't know if I agree, consider a plain vanilla getter and setter:
class Number{ public: Number(int val =0):m_value(val){} int value() const {return m_value;} void setValue(int val) {m_value=val;} private: int m_value; };
value()
(unless the compiler does some magic and makes it atomic but let's say it does not) is not thread safe (a thread could callsetValue
while another callsvalue
), but I wouldn't expect anyone to require me to make it safe (either with a read/write lock or making it atomic) -
To make it clear - I wasn't criticizing anyone. What I wrote was more brainstorming than a response. I had been thinking about these const things for some days and the original thread gave a good reason to write out something. However, the discussion gave a quick impression that someone could use const for thread safety and even rely on that. There's now some heavy food for thought for me here, so many thanks to both of you.
-
Throwing in one more thing to consider. Pointers:
class ClassA{ public: void nonConstMethod() { qDebug("running nonConstMethod");} }; class ClassB1{ ClassA a; public: void nonConstMethod(){ a.nonConstMethod(); // ok } void constMethod() const{ a.nonConstMethod(); // fail not const in const method } };
Nothing strange so far but check what happens if we move
a
from stack to heap:class ClassB2{ ClassA* a; ClassB2() :a(new ClassA()){} ~ClassB2() {delete a;} void nonConstMethod(){ a->nonConstMethod(); // ok } void constMethod() const{ a->nonConstMethod(); // ok } };
What's going on?! it's simple if you think about it: inside
ClassB2::constMethod
,a
behaves like aClassA* const
not like aconst ClassA*
so calling non-const methods is allowed