Re: extending c++ classes and enumerations
Greg Herlihy wrote:
Lourens Veen wrote:
perrog@gmail.com wrote:
However, what I had in mind that if encapsulation is such a great
barrier against class extensions,
Who says it is?
I believe that the reference here is to the apparently widely-held
(though erroneous) belief, evident in many of the posts to this
thread, that support for extending class interfaces would undermine
class encapsulation in C++. Such fears are misplaced; in fact, the
effect would be entirely the opposite. Class extensions, properly
specified, would support class encapsulation to a greater extent
than C++ currently allows.
Well, the OP explicitly stated that his extensions would have access
to private members of the original class:
"Or simply; extension is part of the encapsulation and hence have
access to private, protected and public members as its original
class. Or: extension uses the friend mechanism like other users of
the class do."
That is what I am protesting. It's obvious that if the added functions
only use the public interface then there is no problem; then you're
playing by the rules.
But extending classes in C++ need not break their encapsulation. To
understand why, it is necessary to distinguish between two different
traits that can be used to describe any C++ function. The first is
whether the function is a member of a class interface, and the
second is whether the function's implementation has access to that
class's non-public members. Now the two traits are often conflated
(and treating the two as synonymous leads to a misunderstanding of
class extensions as a feature).
The two traits however are independent, and C++ recognizes the
difference even today. After all, a "friend" of a class is one that
possesses the latter trait (access to non-public class members) but
lacks the former one (membership in the class's interface). Note
further though that the distinction is lopsided: specifically, the
mirror image of the "friend" concept is absent from the C++
language. There is no "relative" keyword expressing the concept that
is the mirror of a "friend". A "relative" (if the concept were to
exist) would be a member of a class interface but one which has not
been granted access to the class's non-public members.
But that wouldn't solve anything with respect to extendability if
relatives, like friends, had to be declared explicitly in the
original class.
Improved Support for Encapsulation
Class relatives would allow a program to decrease the number of
routines with privileged access to a class's private members - even
as they are likely to increase the number of class interface members
overall. Today, a great many classes (such as std::string) include a
significant number of members that do not require privileged access
to the class's non-public members, but which enjoy such privileges
nonetheless. There is no language support to deny privileged class
access to a class member. Class relatives would furnish that
ability: a function could be a member of a class interface without
also enjoying access to its non-public members. And the more
restricted the access to a class's private data, the better that
class has been encapsulated.
// I've never actually written a string class, so the following is
// probably a bad way to do it. But it's not meant to be a working
// string class anyway, just a useful example.
class string_state {
char * start_, * end_;
public:
string_state(char * s, char * e) : start_(s), end_(e) {}
char * begin() const { return start_; }
char * end() const { return end_; }
};
class string {
string_state state_;
public:
string() : state_(0, 0) {}
string(const char * s) : state_(s, s + std::strlen(s)) {}
/* relative */
void uppercase() {
for (char * i = state_.begin(); i != state_.end(); ++i) {
*i = std::toupper(*i);
}
}
};
Now I can change string_state's implementation without worrying about
uppercase():
class string_state {
char * start_;
int len_;
public:
string_state(char * s, char * e) : start_(s), len_(e - s) {}
char * begin() const { return start_; }
char * end() const { return start_ + len_; }
};
I really don't have enough experience to be making these statements,
but as I said in my other post, I think that if your class needs such
protections within itself then its implementation needs to be split
up.
More Focussed Class Interfaces
Let's return to our earlier example: std::string. Now nearly every
one of std::string's member functions could be implemented outside
of std::string itself. But implementing those methods as free
functions would not be an improvement. Because any class interface
that combines free and member functions is one that offers two
different syntaxes with little to account for the difference.
Imagine s.find("abc") and rfind(s, "abc") as members of
std::string's interface. At the very least, the fact that we could
contemplate the possibility should be enough to tell us that there
has to be a better approach. There must be something missing in the
current C++ language that would avoid such a predicament - and in
fact, we already know what that something is. Incidentally, it is
the absent support for class relatives that leads to the
oft-repeated recommendation to prefer free functions to member
functions when designing a class interface - and to do so despite
the obvious cost to the class's usability.
Agreed. Would it be enough to add the ability to call a free function
that takes a T & as its first parameter using member function syntax?
In the above, string_state could then be renamed string, uppercase
could become
void uppercase(string & s) {
for (char * i = state_.begin(); i != state_.end(); ++i) {
*i = std::toupper(*i);
}
}
and then we could write
int main() {
string s("Hello, World!");
// uppercase(s);
s.uppercase(); // sugar
}
Fewer Needless Subclasses
<snip>
We already considered free function as interface extensions. Another
proposed method of adding to an interface - through subclassing - is
even less adequate as a solution. For one, a std::string subclass
provides only a limited solution: legally the added methods can only
be used with derived class objects and not with std::strings objects
in general. Creating subclasses is also likely to become unworkable
as a solution if other modules adopt the same practice and subclass
std::string to add their own custom methods.
Good point, I hadn't thought of that. My experience is only in writing
new (and not very large) programmes, which leaves me with a blind
spot in the area of maintenance of and changes in large systems.
But the primary
objection is simply on the principles of object-oriented programming
itself: a derived class refines the concept expressed in the base
class. Therefore a derived class that exists only to implement a
method that the base could implement just as well by itself (but
happens not to) is a misuse of inheritance.
From a functional point of view, a string that can be uppercased
really is a refined version of a string, since you can do anything
with it that you can do with a string, and then some.
From an existential point of view, the two store the same data and
model the same kind of thing, and there is nothing inherent in a
string that makes it impossible to uppercase the string.
For value classes, which a string class certainly is, I agree that the
latter is the better way of looking at it. If the class models some
behavioural agent, the former may be more expedient.
In general, I'm not quite convinced that we need anything beyond
syntactic sugar for calling specific free functions as if they were
member functions. I'm not familiar enough with the details of
function pointers to know whether a similar mapping between
appropriate function pointers and member function pointers could be
created without breaking anything, but it probably would be nice to
have for regularity.
Lourens
--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]