2112. User-defined classes that cannot be derived from

Section: 20.5.5 [conforming], 23.10.9 [allocator.traits], 23.13.1 [allocator.adaptor.syn] Status: C++14 Submitter: Daniel Krügler Opened: 2011-11-30 Last modified: 2016-02-10

Priority: 1

View all other issues in [conforming].

View all issues with C++14 status.

Discussion:

It is a very established technique for implementations to derive internally from user-defined class types that are used to customize some library component, e.g. deleters and allocators are typical candidates. The advantage of this approach is to possibly take advantage of the empty-base-class optimization (EBCO).

Whether or whether not libraries did take advantage of such a detail didn't much matter in C++03. Even though there did exist a portable idiom to prevent that a class type could be derived from, this idiom has never reached great popularity: The technique required to introduce a virtual base class and it did not really prevent the derivation, but only any construction of such a type. Further, such types are not empty as defined by the std::is_empty trait, so could easily be detected by implementations from TR1 on.

With the new C++11 feature of final classes and final member functions it is now very easy to define an empty, but not derivable from class type. From the point of the user it is quite natural to use this feature for types that he or she did not foresee to be derivable from.

On the other hand, most library implementations (including third-party libraries) often take advantage of EBCO applied to user-defined types used to instantiate library templates internally. As the time of submitting this issue the following program failed to compile on all tested library implementations:

#include <memory>

struct Noop final {
 template<class Ptr>
 void operator()(Ptr) const {}
};

std::unique_ptr<int, Noop> up;

In addition, many std::tuple implementations with empty, final classes as element types failed as well, due to a popular inheritance-based implementation technique. EBCO has also a long tradition to be used in library containers to efficiently store potentially stateless, empty allocators.

It seems that both user and library did the best they could: None of the affected types did impose explicit requirements on the corresponding user-defined types to be derivable from (This capability was not part of the required operations), and libraries did apply EBCO whereever possible to the convenience of the customer.

Nonetheless given the existence of non-derivable-from class types in C++11, libraries have to cope with failing derivations. How should that problem be solved?

It would certainly be possible to add weazel wording to the allocator requirements similar to what we had in C++03, but restricted to derivation-from requirements. I consider this as the bad solution, because it would add new requirements that never had existed before in this explicit form onto types like allocators.

Existing libraries presumably will need internal traits like __is_final or __is_derivable to make EBCO possible in the current form but excluding non-derivable class types. As of this writing this seems to happen already. Problem is that without a std::is_derivable trait, third-party libraries have no portable means to do the same thing as standard library implementations. This should be a good reason to make such a trait public available soon, but seems not essential to have now. Further, this issue should also be considered as a chance to recognice that EBCO has always been a very special corner case (There exist parallels to the previously existing odd core language rule that did make the interplay between std::auto_ptr and std::auto_ptr_ref possible) and that it would be better to provide explicit means for space-efficient storage, not necessarily restricted to inheritance relations, e.g. by marking data members with a special attribute.

At least two descriptions in the current standard should be fixed now for better clarification:

  1. As mentioned by Ganesh, 23.10.9 [allocator.traits] p1 currently contains a (non-normative) note "Thus, it is always possible to create a derived class from an allocator." which should be removed.

  2. As pointed out by Howard, the specification of scoped_allocator_adaptor as of 23.13.1 [allocator.adaptor.syn] already requires derivation from OuterAlloc, but only implies indirectly the same for the inner allocators due to the exposition-only description of member inner. This indirect implication should be normatively required for all participating allocators.

[2012, Kona]

What we really need is a type trait to indicate if a type can be derived from. Howard reports Clang and libc++ have had success with this approach.

Howard to provide wording, and AJM to alert Core that we may be wanting to add a new trait that requires compiler support.

[2014-02, Issaquah: Howard and Daniel comment and provide wording]

Several existing C++11 compilers do already provide an internal __is_final intrinsic (e.g. clang and gcc) and therefore we believe that this is evidence enough that this feature is implementable today.

We believe that both a simple and clear definition of the is_final query should result in a true outcome if and only if the current existing language definition holds that a complete class type (either union or non-union) has been marked with the class-virt-specifier final — nothing more.

The following guidelines lead to the design decision and the wording choice given below:

It has been expressed several times that a high-level trait such as "is_derivable" would be preferred and would be more useful for non-experts. One problem with that request is that it is astonishingly hard to find a common denominator for what the precise definition of this trait should be, especially regarding corner-cases. Another example of getting very differing points of view is to ask a bunch of C++ experts what the best definition of the is_empty trait should be (which can be considered as a kind of higher-level trait).

Once we have a fundamental trait like is_final available, we can easily define higher-level traits in the future on top of this by a proper logical combination of the low-level traits.

A critical question is whether providing such a low-level compile-time introspection might be considered as disadvantageous, because it could constrain the freedom of existing implementations even further and whether a high-level trait would solve this dilemma. We assert that since C++11 the static introspection capabilities are already very large and we believe that making the presence or absence of the final keyword testable does not make the current situation worse.

Below code example demonstrates the intention and the implementability of this feature:

#include <type_traits>

namespace std
{

template <class T>
struct is_final
  : public integral_constant<bool, __is_final(T)>
{};

}  // std

// test it

union FinalUnion final { };

union NonFinalUnion { };

class FinalClass final { };

struct NonFinalClass { };

class Incomplete;

int main()
{
  using std::is_final;
  static_assert( is_final<const volatile FinalUnion>{}, "");
  static_assert(!is_final<FinalUnion[]>{}, "");
  static_assert(!is_final<FinalUnion[1]>{}, "");
  static_assert(!is_final<NonFinalUnion>{}, "");
  static_assert( is_final<FinalClass>{}, "");
  static_assert(!is_final<FinalClass&>{}, "");
  static_assert(!is_final<FinalClass*>{}, "");
  static_assert(!is_final<NonFinalClass>{}, "");
  static_assert(!is_final<void>{}, "");
  static_assert(!is_final<int>{}, "");
  static_assert(!is_final<Incomplete>{}, ""); // error incomplete type 'Incomplete' used in type trait expression
}

[2014-02-14, Issaquah: Move to Immediate]

This is an important issue, that we really want to solve for C++14.

Move to Immediate after polling LEWG, and then the NB heads of delegation.

Proposed resolution:

This wording is relative to N3797.

  1. Change 23.15.2 [meta.type.synop], header <type_traits> synopsis, as indicated

    namespace std {
      […]
      // 20.10.4.3, type properties:
      […]
      template <class T> struct is_empty;
      template <class T> struct is_polymorphic;
      template <class T> struct is_abstract;
      template <class T> struct is_final;
    
      […]
    }
    
  2. Change 23.15.4.3 [meta.unary.prop], Table 49 — Type property predicates, as indicated

    Table 49 — Type property predicates
    Template Condition Preconditions
    template <class T>
    struct is_abstract;
    […] […]
    template <class T>
    struct is_final;
    T is a class type marked with the class-virt-specifier final (12 [class]).
    [Note: A union is a class type that can be marked with final. — end note]
    If T is a class type, T shall be a complete type
  3. After 23.15.4.3 [meta.unary.prop] p5 add one further example as indicated:

    [Example:

    // Given:
    struct P final { };
    union U1 { };
    union U2 final { };
    
    // the following assertions hold:
    static_assert(!is_final<int>::value, "Error!");
    static_assert( is_final<P>::value, "Error!");
    static_assert(!is_final<U1>::value, "Error!");
    static_assert( is_final<U2>::value, "Error!");
    

    end example]