Topic: mutable member data vs. mutable functions
Author: Pierre Baillargeon <pb@artquest.net>
Date: 2000/08/25 Raw View
Carl Nygard wrote:
>
> Is there any reason why the mutable concept wasn't extended to
> functions?
You can get the same behavior by writing a private base class which
declares the member mutable and provides the member functions you would
wish to have your new mutable keyword. Then provide access member
functions that provide const read-access and non-const write access.
Make it all inline if you are concerned with speed.
As for the legitimacy of using mutable in this context, I find it
completely justified, as the fact that the fraction is simplified or not
does not change its logical value. So I disagree with Mr. Nicolet
opinion. What's more, simplifying the fraction at each modification
would slow down calculations considerably, since it would need to be
done at each and every step. It all depends on whether it is part of a
library, which should take reasonable time to provide an efficient
implementation, vs. a single-use class, which can be optimized only if
needed.
---
[ 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 ]
Author: Carl Nygard <cnygard@bellatlantic.net>
Date: 2000/08/21 Raw View
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 ]
Author: David R Tribble <david@tribble.com>
Date: 2000/08/23 Raw View
Carl Nygard wrote:
> Is there any reason why the mutable concept wasn't extended to
> functions? For example, given the example:
> [...]
> 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.
I'm not sure this is a good idea. It would give us three kinds of
member functions:
a. Regular member functions (non-const, non-mutable) that (by virtue
of not being declared const) are assumed to modify one or more
data members.
b. Const member functions, that are assumed to not modify any
non-mutable data members.
c. Mutable member functions, that are assumed to modify only mutable
data members.
I can't see a big difference between (a) and (c).
--
David R. Tribble, mailto:david@tribble.com, http://david.tribble.com
---
[ 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 ]
Author: "Jean-Daniel Nicolet" <jean-daniel.nicolet@finlab.ch>
Date: 2000/08/24 Raw View
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" <cnygard@bellatlantic.net> 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 ]