From 180421553006697000 X-Google-Language: ENGLISH,ASCII-7-bit X-Google-Thread: f78e5,1e33266bee377e X-Google-Attributes: gidf78e5,public From: "Jean-Daniel Nicolet" Subject: Re: mutable member data vs. mutable functions Date: 2000/08/24 Message-ID: <39a41025@news.deckpoint.ch> X-Deja-AN: 661974089 Approved: Fergus Henderson , moderator of comp.std.c++ References: <39A14420.F81235AA@bellatlantic.net> X-Original-NNTP-Posting-Host: 194.38.184.198 X-Priority: 3 X-Authentication-Warning: mulga.cs.mu.OZ.AU: fjh set sender to devnull@stump.algebra.com using -f X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2919.6600 X-Complaints-To: abuse@demon.net X-Mail2News-Path: news.demon.net!mulga.cs.mu.oz.au X-Trace: mail2news.demon.co.uk 967128422 mail2news:13295 mail2news mail2news.demon.co.uk X-Robomod: STUMP, ichudov@algebra.com (Igor Chudov) X-MSMail-Priority: Normal Newsgroups: comp.std.c++ The example you use to illustrate your argument seems a little biased to me. Let me explain. A good design of the Fraction class should not make the members mutable. Before we see how, let's explain what this implies with the methods. Simplify cannot be const anymore (unless we make use of the ugly const_cast feature). The consequence is that the Simplify method must only be called from within non-const methods. This may potentially reduce to only the destructor (not very interesting since objects will not exist anymore after its execution) and the constructor. This leads me to the introduction of a new idea: design the class in such a way as to not be obliged to simplify the fraction anymore after its construction. In other words, I introduce a class invariant saying that any non-const method should let the fraction in a simplified state. Hence the problem is solved. no more mutable members needed. The Simplify method becomes a helper function needed by the non-const methods that must ensure the invariant is restored at the end of their execution. Now to the reason why the members should not be made mutable in the first place. The mutable keyword was introduced in the standard to help compensating the discrepancy between conceptual constness, seen at an abstract level, and the technical constness of concrete members, that is the only constness that can reasonably be checked by the compiler. You may ask why should there be a difference between these two levels. This has to do with the internal state of a given object. Here again we must distinguish between the technical state and the conceptual one. Technically speaking, the internal state of an object is simply a particular combination (bit pattern) of the various memory words occupied by the object. Each time a single bit is changed within the object, we must consider it as a technical state change. On the other (conceptual) side, you, as a client of that object, are not directly interested in its internals, but rather in what you can observe from outside. Depending on which implementation was chosen, there may be for example internally redundant information you don't care about, but that must be maintained by the object itself. A good example is an internal caching strategy. Although as a client you ask the object to give you back a computed value through a (conceptually) const method, the object is forced to change its internal state the first time you ask the value (i.e. to initialize the cache). Because everybody agrees that the computation method should be declared const as long as it computes its result from internal data accessed themselves through const accessors, there is no other way to implement a cache at a later stage without violating the technical constness somehow (either through a const-cast, or more elegantly by declaring the cache as mutable). Viewed from outside, we tend to consider both implementations (with or without cache) as equivalent, hence an implementation decision (adding a cache) that can moreover be taken after the first version has been released should not break the const-ness property of the computing method, hence the introduction of the mutable keyword. Mathematically speaking, the conceptual state may be viewed as an equivalence class of technical states, i.e. regarding different technical states as the same conceptual one from outside. The constness property should then be applied to all methods retaining the conceptual state, not to the methods retaining only the technical state. The proper way to achieve that is to declare all internal members that are conceptually redundant as mutable. That means that the set of attributes must be partitioned in two: The "fundamental" ones, for which a single change automatically implies a conceptual state change, and the "redundant" ones, whose change alters the technical state but not the conceptual one. The later should be declared mutable and the constness can be properly applied to all conceptually const methods. There are cases where the partition is not as easily achieved as in the cache example, where the cache itself clearly belongs to the second category. The fraction example is precisely such a case. The distinction between technical state and conceptual state does not rely on a partition of the variables, but on a value segregation. The conceptual state may be defined as an equivalent class of technical values, and the best choice is clearly to use the mathematical definition of a fraction: All fractions differing only by a multiplication factor applied simultaneously to the numerator and to the denominator should be regarded as equivalent. So we design an helper method implementing the reduction step (or the equivalence reduction, if you prefer) and we introduce an additional constraint on the class, known as a class invariant: we only allow technical states that match with conceptual states with the help of our reduction method, hence we need no mutable members anymore. In summary: - Use mutable members only for "redundant" members (cache, smart pointers, etc.) - Define clearly what your conceptual states are. - Apply the constness property in a consistent manner to every method that does not change the conceputal state of the object. - If you still need const-casts, you probably need to redesign your class, either to achieve the right partinioning among the members, or by chosing a better class invariant. - If you need a class invariant, be sure to have an helper method implementing the reduction step (the "simplify" in the fraction example), plus a "checkInvariant" method launching an exception that you can call at the end of every non-const method. If you apply the above rules consistently, you should probably not need mutable methods anymore! Jean-Daniel Nicolet Software Consultant Geneva Switzerland "Carl Nygard" wrote in message news:39A14420.F81235AA@bellatlantic.net... > > > Is there any reason why the mutable concept wasn't extended to > functions? For example, given the example: > > class Fraction { > public: > Fraction(int num, int denom) : _num(num), _denom(denom) {} > // operator() can modify members, even though const > // Compiler won't flag > double operator()() const { return (double)_num/(double)_denom; } > // Simplify must be able to modify members, even though value > // doesn't change > void Simplify() const { > // find LCD, reduce > } > private: > mutable int _num, _denom; > }; > > In order for Simplify() to be const, both _num and _denom must be > mutable. However, this makes all members of Fraction mutable, > effectively eliminating any type checking for const/non-const. > > Is there some way for the Standard to be tweaked to allow the mutable > keyword for functions, such that only functions marked mutable can > change mutable members. To external functions, mutable is the same as > const, but internally, only mutable functions may change mutable > members. > > > class Fraction { > public: > Fraction(int num, int denom) : _num(num), _denom(denom) {} > // operator() can't modify members > double operator()() const { return (double)_num/(double)_denom; } > // Simplify must be able to modify members, even though value > // doesn't change > void Simplify() mutable { > // find LCD, reduce > } > private: > mutable int _num, _denom; > }; > > --- > [ comp.std.c++ is moderated. To submit articles, try just posting with ] > [ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ] > [ --- Please see the FAQ before posting. --- ] > [ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ] > --- [ comp.std.c++ is moderated. To submit articles, try just posting with ] [ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ] [ --- Please see the FAQ before posting. --- ] [ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html ]