document number: N2852=09-0042
Ville Voutilainen <Ville.Voutilainen@ixonos.com>
Alisdair Meredith <public@alisdairm.net>
Jens Maurer <Jens.Maurer@gmx.net>
Chris Uzdavinis <cuzdav@gmail.com>
2009-03-20

Explicit Virtual Overrides

This paper directly addresses ballot comments US-41 and FI-1 registered against the CD Ballot for the revised C++ standard, N2800.

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.

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. This resolved the viral problem by marking the derived class instead of the base, and that is more frequently under the developer's control. Feedback was to advance to Core once a suitable mark-up could be found.

Attributes for C++0x were initially proposed in N2236 and subsequently adopted in N2751. They provide a suitable markup mechanism without overloading existing keywords or introducing new ones.

N2365 was produced from feedback at the Toronto 2007 meeting, adopting the attribute syntax. The main change was a direct request to solve the similar problems with name-hiding. There remained some reservation that while any program marked up with the feature that successfully translated would have identical meaning (the attributes purely add additional checks and do not affect semantics) there were still some subtle changes in the definition of the virtual keyword.

This paper resolves the outstanding issues in direct response to national body comments from the first CD ballot. The notion of an explicitly polymorphic class is no longer needed, and a new attribute is introduced to explicitly mark a function that should override, rather than add subtle extra meaning to the virtual keyword.

Proposed Solution

We propose to introduce three new attributes to support compile-time checks that name hiding and virtual function overrides occur exactly as intended.

The [[override]] attribute indicates that a member function override a virtual function in at least one of its direct or indirect base classes. If no appropriate virtual function can be found, the program is ill-formed.

The [[hiding]] attribute indicates that a class member hides a name found in at least one of its direct or indirect base classes. If no members with such a name are found, the program is ill-formed.

The [[check_names]] attribute for a class definition indicates that any name hiding or virtual function overriding within that class shall be marked with the appropriate attribute. If there is any 'implicit' overriding or hiding then the program is ill-formed.

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 meaning of a program does not change if a compiler ignores the attributes
  • 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 : base {
      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 such a marked class that intentional overrides use the [[override]] attribute.

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

    Mis-spellings 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 the wrong data type (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 : base {
      void sone_func1 [[override]] ();  // error, mis-spelled name
      void some_func2 [[override]] (double); // error, bad argument type
      void some_func3 [[override]] (); // error, missing cv-qualification
      int some_func4 [[override]] (int); // ill-formed: return type does not match B::h
    };
    

    Introducing new virtual functions

    In an earlier version of this paper, a [[new]] attribute was necessary to introduce a new virtual function into a name-checked class. The addition of the [[override]] attribute means that this is no longer necessary, and this paragraph is mostly to demonstrate that no features have been lost. A new virtual function is automatically introduced by using the virtual keyword and not supplying the [[override]] attribute.

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

    We do not propose a facility to introduce a virtual function with an identical signature to that of a virtual function in a base class, that hides rather than overrides the base member. This facility is available in other languages, but adding it to C++ at this late stage would be disruptive, and violate one of the goals of the attribute form of this proposal: Any program that compiles with the attributes will behave exactly as if they were not present. They are a pure syntax-checking device, and have no impact on semantics.

    Deliberate name hiding

    Another problem that catches people out when defining classes is that introducing a name will hide occurrences of the same name from any base 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 some_func1();
      virtual void some_func2(float);
      virtual void some_func3() const;
      virtual long some_func4(int);
      virtual void f();
      virtual void h(int);
      void j(int);
      void k(int);
    };
    
    class D [[check_names]] : public B {
      using B::j;
      void sone_func1 [[override]] ();  // ill-formed: mis-spelled name
      void some_func2 [[override]] (double); // ill-formed: bad argument type
      void some_func3 [[override]] (); // ill-formed: missing cv-qualification
      int some_func4 [[override]] (int); // ill-formed: return type does not match B::some_func4
      virtual void f [[override]] (); // OK: overrides B::f
      virtual void g(long);      // new virtual function introduced
      void h(int);               // ill-formed: overriding without [[override]]
      virtual void h(double);    // ill-formed: new virtual function hides void h(int)
      virtual void h [[hiding]] (char *);  // OK, new virtual function hides base
      virtual   int j( double );           // OK, using declaration prevents hiding
      int k( double );           // ill-formed: name hiding and no using declaration
      double k [[hiding]] ( char * ); // OK, hiding is clearly indicated
      double m [[hiding]] ( char * ); // ill-formed, hiding is requested, but not present
    };
    

    Corner cases

    A really determined user should be able to combine the [[hiding]] and [[override]] attributes although it seems unlikely to be used in practice beyond test suites.

    struct A {
      virtual void fn();
    };
    
    struct B : A {
      void fn [[hiding]] (int);
    };
    
    struct C : B {
      void fn [[override, hiding]]();
    };
    

    Proposed Wording

    Add a new section 7.6.5(new) dcl.attr.override between the current sections 7.6.4 and 7.6.5:
    7.6.5(new) Class member name checking attributes

    The attribute-token override asserts that a virtual member function overrides a function in a base class. It shall appear at most once in each attribute-list and no attribute-argument-clause shall be present. The attribute applies to virtual member functions being declared in a class definition.

    If a virtual member function f is marked override and does not override (10.3 class.virtual) a member function of a base class the program is ill-formed.

    The attribute-token hiding asserts that a class member name hides a name in a base class. It shall appear at most once in each attribute-list and no attribute-argument-clause shall be present. The attribute applies to class members being declared in a class definition.

    If a class member is marked hiding and its name does not hide (3.3.10 basic.scope.hiding, 10.2 class.member.lookup) a class member name in a base class the program is ill-formed.

    The attribute-token check_names specifies that a class strictly checks overriding and hiding of base members. 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.

    In a class definition marked check_names, if a virtual member function overrides (10.3 class.virtual) a member function of a base class and it is not marked override, the program is ill-formed. Similarly, in such a class definition, if a class member name hides (3.3.10 basic.scope.hiding, 10.2 class.member.lookup) a class member name in a base class and it is not marked hiding, the program is ill-formed. [ Note: A using-declaration makes the potentially hidden name visible, avoiding the need for the hiding attribute. -- end note ]

    [Example:

     class B {
        virtual void some_func();
    
        virtual void f(int);
        virtual void h(int);
        void j(int);
        void k();
     };
    
     class D [[check_names]] : public B {
        void sone_func [[override]] ();          // error: mis-spelled name
    
        void f [[override]] (int);               // ok: f implicitly virtual, overrides B::f
        virtual void f [[override]] (long);      // error: non-matching argument type
        virtual void f [[override]] (int) const; // error: non-matching cv-qualification
        virtual int  f [[override]] (int);       // error: non-matching return type
    
        virtual void g(long);      // ok: new virtual function introduced
    
        void h(int);               // error: h implicitly virtual, but overriding without marker
        virtual void h(double);    // error: hides B::h without marker
        virtual void h [[hiding]] (char *);      // ok
    
        using B::j;
        int j(double);                   // ok: not hiding due to "using"
        void j(int);                     // ok, despite 'obscuring' B::j(int)
        virtual int j [[hiding]] (void); // error: not hiding due to "using"
    
        int k;                           // error: hides B::k without marker
    
        int m [[hiding]] ( int );        // error: no hiding despite marker
     };
    	
    end example ]

    Change section 12.4 class.copy paragraph 6 as indicated:
    ... If a class has a base class with a virtual destructor, its destructor (whether user- or implicitly- declared) is virtual and marked override (7.6.5(new) dcl.attr.override).
    Change section 12.8 class.copy paragraph 10 as indicated:
    ... An implicitly-declared copy assignment operator for a class X is marked hiding (7.6.5(new) dcl.attr.override) if X has a base class. Because a copy assignment operator is implicitly declared for a class if not declared by the user, ...