开发者

name hiding and fragile base problem

开发者 https://www.devze.com 2023-03-04 08:53 出处:网络
I\'ve seen it stated that C++ has name hiding for the purposes of reducing the fragile base class problem. However, I definitely don\'t see how this helps. If the base class introduces a function or o

I've seen it stated that C++ has name hiding for the purposes of reducing the fragile base class problem. However, I definitely don't see how this helps. If the base class introduces a function or overload that previously did not exist, it might conflict with those introduced by the derived class, or unqualified calls to global functions or member functions- but what I don't see is how this is different for overloads. Why should overloads of virtual functions be treated differently to, well, any other function?

Edit: Let me show you a little more what I'm talking about.

struct base {
    virtual void foo();
    virtual void foo(int);
    virtual void bar();
    virtual ~base();
};
struct derived : base {
    virtual void foo();
};

int main() {
    derived d;
    d.foo(1); // Error- foo(int) is hidde开发者_如何学运维n
    d.bar(); // Fine- calls base::bar()
}

Here, foo(int) is treated differently to bar(), because it's an overload.


I'll assume that by "fragile base class", you mean a situation where changes to the base class can break code that uses derived classes (that being the definition I found on Wikipedia). I'm not sure what virtual functions have to do with this, but I can explain how hiding helps avoid this problem. Consider the following:

struct A {};

struct B : public A
{
    void f(float);
};

void do_stuff()
{
    B b;
    b.f(3);
}

The function call in do_stuff calls B::f(float).

Now suppose someone modifies the base class, and adds a function void f(int);. Without hiding, this would be a better match for the function argument in main; you've either changed the behaviour of do_stuff (if the new function is public), or caused a compile error (if it's private), without changing either do_stuff or any of its direct dependencies. With hiding, you haven't changed the behaviour, and such breakage is only possible if you explicitly disable hiding with a using declaration.


I don't think that overloads of virtual functions are treated any differently that overloads of regular functions. There might be one side effect though.

Suppose we have a 3 layers hierarchy:

struct Base {};

struct Derived: Base { void foo(int i); };

struct Top: Derived { void foo(int i); }; // hides Derived::foo

When I write:

void bar(Derived& d) { d.foo(3); }

the call is statically resolved to Derived::foo, whatever the true (runtime) type that d may have.

However, if I then introduce virtual void foo(int i); in Base, then everything changes. Suddenly Derived::foo and Top::foo become overrides, instead of mere overload that hid the name in their respective base class.

This means that d.foo(3); is now resolved statically not directly to a method call, but to a virtual dispatch.

Therefore Top top; bar(top) will call Top::foo (via virtual dispatch), where it previously called Derived::foo.

It might not be desirable. It could be fixed by explicitly qualifying the call d.Derived::foo(3);, but it sure is an unfortunate side effect.

Of course, it is primarily a design problem. It will only happen if the signature are compatible, else we'll have name hiding, and no override; therefore one could argue that having "potential" overrides for non-virtual functions is inviting troubles anyway (dunno if any warning exist for this, it could warrant one, to prevent being put in such a situation).

Note: if we remove Top, then it is perfectly fine to introduce the new virtual method, since all old calls were already handled by Derived::foo anyway, and thus only new code may be impacted

It is something to keep in mind though when introducing new virtual methods in a base class, especially when the impacted code is unknown (libraries delivered to clients).

Note that C++0x has the override attribute to check that a method is truly an override of a base virtual; while it does not solve the immediate problem, in the future we might imagine compilers having a warning for "accidental" overrides (ie, overrides not marked as such) in which case such an issue could be caught at compile-time after the introduction of the virtual method.


In The Design and Evolution of C++, Bjarne Stroustrup Addison-Weslay, 1994 section 3.5.3 pp 77, 78, B.S. explains that the rule by which a name in a derived class hides all definition of the same name in its base classes is old and dates back from C with Classes. When it was introduced, B.S. considered it as the obvious consequence of scoping rules (it's the same for nested blocks of code or nested namespaces — even if namespace were introduced after). The desirability of its interactions with overloading rules (the overloaded set doesn't contain the function defined in the base classes, nor in the enclosing blocks — now harmless as declaring functions in block is old fashioned —, nor in enclosing namespaces where the problem occasionally strikes as well) has been debated, to the point that G++ implemented alternative rules allowing the overloading, and B.S. argued that the current rule helps preventing errors in situations like (inspired from real live problems with g++)

class X {
   int x;
public:
   virtual void copy(X* p) { x = p->x; }
};

class XX: public X {
   int xx;
public:
   virtual void copy(XX* p) { xx = p->xx; X::copy(p); }
};

void f(X a, XX b) 
{
   a.copy(&b); // ok: copy X part of b
   b.copy(&a); // error: copy(X*) is hidden by copy(XX*)
}

Then B.S. continues

In retrospect, I suspect that the overloading rules introduced in 2.0 might have been able to handle this case. Consider the call b.copy(&a). The variable b is an exact type match for the implicit argument of XX::copy, but requires a standard conversion to match X::copy. The variable a on the other hand, is an exact match for the explicit argument of X::copy, but requires a standard conversion to match XX:copy. Thus, had the overloading been allowed, the call would have been an error because it is ambiguous.

But I fail to see where the ambiguity is. It seems to me that B.S. overlooked the fact that &a can't be implicitly converted to a XX* and thus only X::copy has be considered.

Indeed trying with free (friends) functions

void copy(X* t, X* p) { t->x = p->x; }
void copy(XX* t, XX* p) { t-xx = p->xx; copy((X*)t, (X*)p); }

I get no ambiguity error with current compilers and I don't see how rules in the Annotated C++ Reference Manual would makes a difference here.

0

精彩评论

暂无评论...
验证码 换一张
取 消