document number: N2365=07-225
Jens Maurer <Jens.Maurer@gmx.net>
Alisdair Meredith <public@alisdairm.net>
2007-08-06

Explicit Virtual Overides

Problem Summary

We would like to tighten the rules for overriding virtual functions, to detect these problems: We are deliberately not solving the problem that a base class B1 has a virtual function f, another base class B2 also has a virtual function f with the same signature, the user derives a class D from both B1 and B2 and declares D::f to override both B1::f and B2::f, one of which may have been inadvertent. (This is sometimes referred to as the Siamese Twins problem.)

The scope of the problem was reduced to the minimal necessary to solve the common real-world problems. This means it does not tackle ideas such as 'final overriders' or function renaming, both of which would be available if a scheme closer to the C++/CLI standard was pursued.

History

N1827 was presented at the Mont Tremblant meeting as a first pass at this problem. Feedback was positive, but concerned about the 'viral' nature of that proposal. It marked the base class as special and implied that all descendants must be treated as special as well, even when not marked. This viral problem is resolved by marking the derived class instead, as that is more frequently under the developer's control.

N2108 was presented at the Portland meeting and introduced the concept of an explicit polymorphic class as a refinement of the existing polymorphic class type. The virtual function syntax wording could then be updated to use the new class type, while deferring the final choice of syntax to mark the class as explicit to a later paper. Feedback was to advance to Core once a suitable markup could be found.

This paper adopts the notation of the attributes proposal for C++0x in N2236. It also includes feedback since Portland to address name-hiding in addition to the virtual function issues.

Proposed Solution

We propose to introduce a [[check_names]] attribute for class definitions. Use of this attribute turns on additional checks that the member functions declared in the class do not conflict with the those of names in any base class.

In particular, the "virtual" keyword must be used when overriding virtual functions, and named declared in a base class cannot be hidden. Two further attributes are introduced that allow specific functions to disable the checks. [[new]] indicates that a virtual function is being introduced for the first time, and [[hiding]] indicates a deliberate attempt to hide names from a base class.

This design meets three key compatibility requirements:

  • Use of the attributes does not depend on their being used in a base class
  • Use of the attributes in a library class is invisible to users of that class
  • The meaining of a program does not change if a compiler ignores the attributes
  • It also meets an additional goal that the number of attribute mark-ups in typical use is small, often no more than a single attribute on the class definition. This is based on an assumption that name hiding is rarely intentional, and will be typically fixed with a using declaration. It is also assumed that virtual functions are more frequently overriden than introduced. This is especially true with the common idiom of an 'interface class' where only root-classes introduces virtual functions. Note that roots, having no base classes, should not be marked up in practice as there is nothing to check for classes without a base.

    Implicit virtual and the Accidental Override

    The 'simple' syntax for overriding virtual functions in C++ today is convenient, but potentially misleading. Take the following example:

    struct base {
      virtual void some_func();
    };
    
    struct derived {
      void some_func();
    };
    
    

    Is the use of some_func in derived intentionally an override, or an accidental name clash? Without clear documentation or a detailed analysis of the use/implementation it is hard to be sure.
    The common assumption is that all such use is deliberate, and to ignore the question. Yet while it is reasonable to assume that competent developers would not accidentally make such a mistake, it is easily introduced by dependencies on third party libraries. Maintenance of the base classes might introduce such a clash without clear notice in the code, and the ensuing bugs can be very hard to track down. Worse, even a rigorous set of unit tests does not guarantee protection, as it is the base class, not the derived, that is changing behaviour and so the effects are not guaranteed to be covered by existing tests. Likewise, source analysis tools can offer little help here without an additional hint from the coder. To that end, we offer the [[check_names]] attribute, and the requirement in a checked class that intentional overrides use the virtual keyword, which is remains optional when the attribute is not used.

    struct base {
      virtual void some_func();
    };
    
    struct derived [[check_names]] {
      void some_func();  // error, accidental override with check_names attribute
      virtual void some_func(); // OK, override with virtual keyword
    };
    
    

    Mispellings and mistaken signature

    A second set of problems arise when a user intends to override a function, but gets the signature wrong in some way. Common examples are mis-spelling the name (transposing two characters, bad capitalization); using a the wrong datatype (double instead of float); or missing the cv-qualification.

    All these classes of errors can be detected in the same way. When the virtual keyword is used in a class defined with the [[check_names]] attribute, a matching declaration must be found in one of the base classes for it to override. In implementation terms it would suffice to check the inherited vtable, rather than search the full DAG of base classes looking for a match.

    Note: This kind of problem should also be detected with a good set of unit tests. It is hard to solve in a lexical analysis tool without a further hint, see next point!

    struct base {
      virtual void some_func1();
      virtual void some_func2(float);
      virtual void some_func3() const;
      virtual long some_func4(int);
    };
    
    struct derived [[check_names]] {
      virtual void sone_func1();  // error, mis-spelled name
      virtual void some_func2(double); // error, no bad argument type
      virtual void some_func3(); // error, missing cv-qualification
      virtual int some_func4(int); // ill-formed: return type does not match B::h
    };
    

    Introducing new virtual functions

    The problem with checking for functions to override is that is becomes impossible to introduce a new virtual function into the hierarchy in a checked class. We propose to solve this with the [[new]] attribute. This is used with virtual function declations to:

  • allow a new virtual function to be introduced without failing the override check
  • Check that the new signature does not accidentally override a base member
  • The latter check will be diagnosed as ill-formed, rather than introducing a new function that hides the base. This guarantees the feature does nothing more than name checking, and cannot produce a program with a different meaning if the attributes are ignored.

    This kind of check is hard to cover with unit tests, and can only be caught by lexical analysis tools with an additional mark-up, such as we propose here.

    struct base {
      virtual void some_func1();
    };
    
    struct derived [[check_names]] {
      virtual void some_func1(); [[new]] // error, accidental override
      virtual void some_func2(); [[new]] // OK, new virtual function introduced
    };
    

    Deliberate name hiding

    Another problem that catches people out when defining classes is that introducing a name will hide occurences of the same name from any bases classes. Sometimes this is intentional, but there is little chance of a warning when it occurs accidentally. The language-supplied workaround in those cases is a using declaration, but you only apply the workaround when you are aware of the problem. The [[check_names]] attribute will require the compiler inform you of accidental hiding. The [[hiding]] attribute will allow you to tell the compiler that the hiding is intentional, and the program remains well-formed.

    Example:

    class B {
      virtual void f();
      virtual void h(int);
      void j(int);
     void k(int);
    };
    
    class D [[check_names]] : public B {
      using B::j;
      virtual void f();          // OK: overrides B::f
      virtual void g(long);      // ill-formed: new virtual function introduced
      void h(int);               // ill-formed: overriding without "virtual"
      virtual void h(long);      // ill-formed: new virtual function introduced
      virtual void h(double); [[new]] // ill-formed: new virtual function hides void h(int)
      virtual void h(char *); [[new, hiding]] // OK
      virtual   int j( double );           // OK, using declaration prevents hiding
      int k( double );           // ill-formed: name hiding and no using declaration
      double k( char * ) [[hiding]]; // OK, hiding is clearly indicated
    };
    

    Proposed Wording

    Add a new section 7.6.5 dcl.attr.checks:
    7.6.5 Attributes to check names of class members

    The attribute-token check_names specifies that a class strictly checks overriding and hiding of base member functions. It shall appear at most once in each attribute-list and no attribute-argument-clause shall be present. The attribute applies to a class definition, see 10.3 class.virtual.

    The attribute-token new specifies that a virtual member function does not override a function in a base class.

    The attribute-token hiding specifies that the name of a member function hides another member function in a base class.

    Each of the attribute-tokens new and hiding shall appear at most once in each attribute-list and no attribute-argument-clause shall be present for either. The attributes apply to the function type of a member function declaration, but the type of the member function is unchanged. The attribute-token new applies only to the type of a virtual member function.

    Replace in 10.3p2
    If a virtual member function vf is declared in a class Base and in a class Derived, derived directly or indirectly from Base, a member function vf with the same name, parameter-type-list (8.3.5), and cv-qualification as Base::vf is declared, then Derived::vf is also virtual ( whether or not it is so declared) and it overrides Base::vf.
    by
    If a virtual member function vf is declared in a class B and in a class D, derived directly or indirectly from B, a member function vf with the same name is declared, D::vf may override B::vf as follows: If D is not marked with the check_names attribute and the parameter-type-list (8.3.5) and cv-qualification of D::vf are the same as those of B::vf, then D::vf is also virtual (whether or not it is so declared) and it overrides B::vf. If D is marked with the check_names attribute, then D::vf shall be declared virtual and, if the parameter-type-list (8.3.5) and cv-qualification of D::vf are the same as those of B::vf, D::vf overrides B::vf. In a class marked with the check_names attribute, a virtual member function shall be marked with the new attribute if and only if it does not override one in a base class .
    Add a new paragraph to 3.3.8 (basic.scope.hiding)
    If a member-declaration hides a member that would otherwise be visible in a base class, that member-declaration shall be decorated with the hiding attribute (7.6.5 dcl.attr.checks). [Note: A using directive makes the potentially hidden name visible, avoiding the need for the hiding attribute.]