1. Changelog
-
R0
-
First submission
-
2. Motivation and Scope
We propose the addition of the
type trait to the Standard Library, that complements the existing
trait.
The trait aims to detect whether a given class is a virtual base class of another one, ignoring accessibility, ambiguities and cv-qualifiers, just like
.
Detecting whether a class is a virtual or non-virtual base of another class has applications in the context of pointer conversions. Given a pointer
convertible to
(that is,
is an accessible, non-ambiguous base of
), actually performing the conversion requires completely different implementations depending on whether
is a non-virtual or a virtual base of
.
On all common ABIs, if
is a non-virtual base of
, then the conversion consists in a test for nullptr. Otherwise, a constant offset is applied to the pointer; the offset is entirely known at compile time. The test for nullptr is necessary to yield nullptr back in that case; on common ABIs, null pointers to objects are represented by a
value, and we have to return
(= null pointer) and not
to honor [conv.ptr]/3 (null pointers convert to null pointers).
If instead
is a virtual base of
, then we perform the test for nullptr as usual. In case we don’t have a null pointer, we then have to determine the address of the
subobject inside the
object. The actual mechanics depend on the ABI, but this always requires inspecting
’s virtual table, which means examining
(the pointee). If the pointee has already been destroyed and the pointer is dangling (an "invalid pointer value", as per [basic.stc.general]/4’s definition), then this will cause problems.
Technically speaking, the result of the cast is implementation-defined; [basic.stc.general]/4 says that "Any other use of an invalid pointer value has implementation-defined behavior". (In particular, we don’t think that the "Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior." sentence applies here, as the indirection is necessary due to implementation/ABI requirements, and not language requirements.)
Although no implementation seems to document their behavior, on all common ABIs, the conversion of an invalid pointer value towards a virtual base causes an illegal memory access (which may segfault, or just read "garbage", etc.); while a conversion towards a non-virtual base class does not cause any issues (see the note below).
This behavior with pointer conversions is taken into account, for instance, in
’s converting constructor. On all major implementations (libstdc++, libc++, MS-STL),
is implemented with this class layout:
template < typename T > class weak_ptr { control_block * m_cb ; T * m_data ; };
Let’s now consider the move-converting constructor from a
to
. Simplifying, the constructor is constrained on
being convertible to
(cf. [util.smartptr.weak.const]/6 and [util.smartptr.shared.general]/6).
The "obvious" implementation:
template < typename Y > requires std :: is_convertible_v < Y * , T *> weak_ptr ( weak_ptr < Y > && other ) : m_cb ( std :: exchange ( other . m_cb , nullptr )), m_data ( std :: exchange ( other . m_data , nullptr )) // <-- danger {}
is wrong and will produce UB / crashes when converting
to
, in case 1)
is a virtual base of
and 2) the managed object has already been destroyed. This is a concrete possibility -- the raison d’être for
is that it does not keep the managed object alive.
A correct implementation therefore needs to check that the managed object is still alive:
template < typename Y > requires std :: is_convertible_v < Y * , T *> weak_ptr ( weak_ptr < Y > && other ) : m_cb ( other . m_cb ), m_data ( other . lock (). get ()) { other . m_cb = nullptr ; other . m_data = nullptr ; }
The above snippet works, but now this is a pessimization for the "common" case when
is a non-virtual base of
. In that case we could avoid the (relatively more expensive)
operation and just safely offset the pointer.
If an implementation wishes to optimize for this case, then they would need to detect if
is a virtual base class of
, which is where they would use the type trait that we’re proposing:
// simple implementation for non-virtual base template < typename Y > requires ( std :: is_convertible_v < Y * , T *> && ! std :: is_virtual_base_of_v < T , Y > ) weak_ptr ( weak_ptr < Y > && other ) : m_cb ( std :: exchange ( other . m_cb , nullptr )), m_data ( std :: exchange ( other . m_data , nullptr )) {} // correct, more expensive implementation for virtual bases template < typename Y > requires ( std :: is_convertible_v < Y * , T *> && std :: is_virtual_base_of_v < T , Y > ) weak_ptr ( weak_ptr < Y > && other ) : m_cb ( other . m_cb ), m_data ( other . lock (). get ()) { other . m_cb = nullptr ; other . m_data = nullptr ; }
Note: On all common Standard Library implementations there is no concern for accessing
even in case it may contain an invalid pointer value ([basic.stc.general]/4). For instance, it’s simply copied as a pointer (and not
’d or similar) in their implementation of
’s move constructor. This confirms our understanding that no major compiler/ABI has any "special" behavior with regards to invalid pointer values, although they don’t document their implementation-specific behavior in the area.
A similar consideration may be applied to
’s converting constructor, which could be marked as
if a conversion towards a virtual base class is requested (since such a conversion may be "unsafe"). We however do not have sufficient field experience with such a change to reach any conclusions.
2.1. Prior art
[Boost.TypeTraits] has shipped a
type trait since at least Boost version 1.40 (2009). A similar implementation has been added to Qt as private API. The trait that we are proposing will however have slightly different semantics.
A trait with the same name and semantics has also been proposed by [N3987], although in a different context (reflection), and together with many more traits. We are unsure of the status of that paper, which looks more like a thought experiment than an actual proposal.
3. Design Decisions
3.1. Do we need this trait in the Standard Library? Can it be implemented entirely in user code?
This trait has been indeed implemented in user code, but the detection achieved this way is incomplete (see below). The implementation is definitely "experts-only", and therefore would benefit from being provided by the Standard Library.
The core of the detection in Boost and Qt combines these two checks:
-
whether
is well formed. If it is, then( Base * ) std :: declval < Derived *> ()
is an unambiguous base ofBase
. This will be true ifDerived
is a virtual base class ofBase
(andDerived
doesn’t also inherit fromDerived
non-virtually). The cast notation is used in order to ignore accessibility ([expr.cast]/4).Base -
whether
is ill formed. This is the case if( Derived * ) std :: declval < Base *> ()
is an ambiguous or virtual base ofBase
(again ignoring accessibility).Derived
With the first check we establish that
is not an ambiguous base, and combined with the second check, we therefore establish that it must be a virtual base.
3.2. What about classes that inherit virtually and non-virtually from the same base?
The detection in usercode has a shortcoming: if
inherits virtually and non-virtually from
, then 1. fails, but
still is "a" virtual base class. Such a situation is perfectly legitimate and allowed by the core language.
Consider for instance the example in Note 6 in [class.mi]:
class B { /* ... */ }; class X : virtual public B { /* ... */ }; class Y : virtual public B { /* ... */ }; class Z : public B { /* ... */ }; class AA : public X , public Y , public Z { /* ... */ };
In this example
inherits from
twice: once virtually (through
and
) and once non-virtually (through
). Therefore, what should
yield? There are two possible lines of reasoning:
-
is a virtual base class (considering all possible inheritance paths); therefore the trait should yieldB true
; -
is not a virtual base class in at least one inheritance path; therefore the trait should yieldB false
.
(In other words: with 1. we would be asking if
is a virtual base class, and with 2. we would be asking if
is a virtual base class and also not a non-virtual base class.)
We believe that the following considerations apply here:
-
Each choice has some merits. 1. is what the core language says; however 2. is what Boost.TypeTraits and similar code actually detect (and have done so for a very long time). We are not aware of any implementation in user code that is able to do detection no. 1, nor viable implementation strategies.
-
Whether the trait does 1. or 2. does not ultimately affect the main use case of the trait (pointer conversions). In the example,
is simply not convertible toAA *
because the conversion is ambiguous, so for those use cases it won’t matter at all what the trait detects.B * -
already somehow deviates from the base class definition in core language, as the core language doesn’t allow a class to be a base of itself ([class.derived.general]/2, "A class-or-decltype shall denote a (possibly cv-qualified) class type that is not an incompletely defined class").std :: is_base_of
In this proposal we feel more comfortable at embracing the core language definition of virtual base class, and therefore we would propose choice no. 1 (that is, for the trait to detect that
is a virtual base class of
), despite the prior art in Boost and Qt.
We also don’t believe it is going to be necessary to offer a trait that does detection no. 2, for two reasons. First, having fine-grained detections sounds more like task for a reflection system rather than for type traits in the standard library. (For instance, there aren’t multiple "versions" of
that check for accessibility, multiple inheritance from the same class, and so on. The trait does one thing and one thing only.) Second, as discussed above, we do not have any use case for it, and would not risk introducing two traits that will just cause confusion to end users.
3.3. Is a class a virtual base of itself?
We are aware that, although no class is a base class of itself,
is actually true
. While one can consider
to model a general inheritance/is-A relationship between types, the check for virtual inheritance is actually much more specific, and we strongly feel that the trait should yield what the core language describes here: no class is a virtual base class of itself.
4. Impact on the Standard
This proposal is a pure library extension; it adds a type trait to
. There are no changes required in the core language.
Vendors are expected to implement the trait through internal compiler hooks, as they already do for other similar type traits (e.g.
), for performance, correctness and maintenance reasons.
5. Technical Specifications
All the proposed changes are relative to [N4958].
6. Proposed wording
Add to the list in [version.syn]:
#define __cpp_lib_is_virtual_base_of YYYYMML // also in <type_traits>
Modify [meta.type.synop] as shown.
Add to first [meta.rel] block:
template < class Base , class Derived > struct is_base_of ; template < class Base , class Derived > struct is_virtual_base_of ;
And to the second block:
template < class Base , class Derived > constexpr bool is_base_of_v = is_base_of < Base , Derived >:: value ; template < class Base , class Derived > constexpr bool is_virtual_base_of_v = is_virtual_base_of < Base , Derived >:: value ;
Insert a new row in Table 49 in [meta.rel], after the one for
:
template < class Base , class Derived > struct is_virtual_base_of ; is a virtual base class of
Base ([class.mi]) without regard to cv-qualifiers.
Derived If and
Base are non-union class types,
Derived shall be a complete type. [Note 2: Virtual base classes that are private, protected or ambiguous are, nonetheless, virtual base classes. — end note] [Note 3: A class is never a virtual base class of itself. — end note]
Derived
7. Acknowledgements
Thanks to KDAB for supporting this work.
All remaining errors are ours and ours only.