Index: include/clang/AST/DeclCXX.h =================================================================== --- include/clang/AST/DeclCXX.h +++ include/clang/AST/DeclCXX.h @@ -473,6 +473,9 @@ /// \brief Whether we are currently parsing base specifiers. bool IsParsingBaseSpecifiers : 1; + /// \brief Whether the class uses the unstable ABI. + bool IsUnstableABI : 1; + /// \brief The number of base class specifiers in Bases. unsigned NumBases; @@ -708,6 +711,11 @@ return data().IsParsingBaseSpecifiers; } + void setIsUnstableCXXABI() { data().IsUnstableABI = true; } + bool isUnstableCXXABI() const { + return data().IsUnstableABI; + } + /// \brief Sets the base classes of this struct or class. void setBases(CXXBaseSpecifier const * const *Bases, unsigned NumBases); Index: include/clang/Basic/Attr.td =================================================================== --- include/clang/Basic/Attr.td +++ include/clang/Basic/Attr.td @@ -1537,6 +1537,18 @@ let Documentation = [Undocumented]; } +def StableABI : InheritableAttr { + let Spellings = [CXX11<"clang", "stable_abi">]; + let Subjects = SubjectList<[Record]>; + let Documentation = [ABIStabilityDocs]; +} + +def UnstableABI : InheritableAttr { + let Spellings = [CXX11<"clang", "unstable_abi">]; + let Subjects = SubjectList<[Record]>; + let Documentation = [ABIStabilityDocs]; +} + def AnyX86Interrupt : InheritableAttr, TargetSpecificAttr { // NOTE: If you add any additional spellings, ARMInterrupt's, // MSP430Interrupt's and MipsInterrupt's spellings must match. Index: include/clang/Basic/AttrDocs.td =================================================================== --- include/clang/Basic/AttrDocs.td +++ include/clang/Basic/AttrDocs.td @@ -1958,3 +1958,20 @@ The system will crash if the wrong handler is used. }]; } + +def ABIStabilityDocs : Documentation { + let Category = DocCatType; + let Content = [{ +The ``[[clang::stable_abi]]`` and ``[[clang::unstable_abi]]`` attributes +control whether Clang has permission to use an unstable ABI for the attached +class. It is an ODR violation to use ``[[clang::unstable_abi]]`` to define +the same class in two translation units compiled with different versions of +Clang. Specifically, mixing different head revisions or major releases is +not allowed, but mixing different point releases is fine. + +Classes inherit ABI from their dynamic base classes (i.e. classes that have +virtual member functions or virtual bases). It is an error to derive from +two or more bases if their ABIs are incompatible, or to use an attribute to +specify an ABI that is incompatible with a base. + }]; +} Index: include/clang/Basic/DiagnosticSemaKinds.td =================================================================== --- include/clang/Basic/DiagnosticSemaKinds.td +++ include/clang/Basic/DiagnosticSemaKinds.td @@ -8325,4 +8325,23 @@ "parameterized class %0 already conforms to the protocols listed; did you " "forget a '*'?">, InGroup; +def err_abi_mismatch : Error< + "inconsistent ABI for class %0">; +def note_stable_abi_base : Note< + "base %0 uses the stable ABI">; +def note_unstable_abi_base : Note< + "base %0 uses the unstable ABI">; +def note_stable_abi_attr : Note< + "stable ABI specified by attribute">; +def note_unstable_abi_attr : Note< + "unstable ABI specified by attribute">; + +def warn_cxx_stable_abi : Warning< + "stable C++ ABI was inferred for class %0">, + InGroup>, DefaultIgnore; +def note_add_unstable_abi_file : Note< + "add %0 to file %1 to silence this warning">; +def note_add_unstable_abi_attr : Note< + "add attribute clang::unstable_abi or clang::stable_abi to silence this warning">; + } // end of sema component. Index: include/clang/Basic/LangOptions.h =================================================================== --- include/clang/Basic/LangOptions.h +++ include/clang/Basic/LangOptions.h @@ -118,6 +118,17 @@ /// host code generation. std::string OMPHostIRFile; + /// List of context names that should use the unstable ABI. This list is + /// sorted. + std::vector UnstableABIContextNames; + + /// If the unstable ABI context names were loaded from a single file, this + /// names the file. + std::string UnstableABIContextNamesPath; + + /// Returns whether the argument names an unstable ABI context. + bool isUnstableABIContextName(const std::string &Name) const; + LangOptions(); // Define accessors/mutators for language options of enumeration type. Index: include/clang/Driver/Options.td =================================================================== --- include/clang/Driver/Options.td +++ include/clang/Driver/Options.td @@ -1116,6 +1116,12 @@ def funsigned_bitfields : Flag<["-"], "funsigned-bitfields">, Group; def funsigned_char : Flag<["-"], "funsigned-char">, Group; def fno_unsigned_char : Flag<["-"], "fno-unsigned-char">; +def funstable_cxx_abi_classes : Flag<["-"], "funstable-c++-abi-classes">, + Group, Flags<[CC1Option]>, + HelpText<"Use the unstable C++ class ABI for all classes">; +def funstable_cxx_abi_classes_EQ : Joined<["-"], "funstable-c++-abi-classes=">, + Group, Flags<[CC1Option]>, + HelpText<"Path to a list of namespaces that automatically use the unstable C++ class ABI">; def funwind_tables : Flag<["-"], "funwind-tables">, Group; def fuse_cxa_atexit : Flag<["-"], "fuse-cxa-atexit">, Group; def fuse_init_array : Flag<["-"], "fuse-init-array">, Group, Flags<[CC1Option]>, Index: include/clang/Sema/Sema.h =================================================================== --- include/clang/Sema/Sema.h +++ include/clang/Sema/Sema.h @@ -9295,6 +9295,20 @@ // Emitting members of dllexported classes is delayed until the class // (including field initializers) is fully parsed. SmallVector DelayedDllExportClasses; + + /// A set of contexts that imply the unstable ABI for that context and any + /// enclosed contexts, excluding named namespaces. + llvm::DenseSet UnstableABIContexts; + + /// A set of contexts that imply the unstable ABI for that context and any + /// enclosed contexts, including namespaces. + llvm::DenseSet UnstableABIGlobContexts; + + /// Determine the ABI for this class using its attributes, bases and implicit + /// contexts. Check for conflicts between bases or between a base and an + /// attribute. Set the class's isUnstableCXXABI() flag according to the + /// result. + void checkClassABI(CXXRecordDecl *RD); }; /// \brief RAII object that enters a new expression evaluation context. Index: lib/AST/DeclCXX.cpp =================================================================== --- lib/AST/DeclCXX.cpp +++ lib/AST/DeclCXX.cpp @@ -70,8 +70,8 @@ ImplicitCopyAssignmentHasConstParam(true), HasDeclaredCopyConstructorWithConstParam(false), HasDeclaredCopyAssignmentWithConstParam(false), IsLambda(false), - IsParsingBaseSpecifiers(false), NumBases(0), NumVBases(0), Bases(), - VBases(), Definition(D), FirstFriend() {} + IsParsingBaseSpecifiers(false), IsUnstableABI(false), NumBases(0), + NumVBases(0), Bases(), VBases(), Definition(D), FirstFriend() {} CXXBaseSpecifier *CXXRecordDecl::DefinitionData::getBasesSlowCase() const { return Bases.get(Definition->getASTContext().getExternalSource()); Index: lib/Basic/LangOptions.cpp =================================================================== --- lib/Basic/LangOptions.cpp +++ lib/Basic/LangOptions.cpp @@ -43,3 +43,8 @@ return true; return false; } + +bool LangOptions::isUnstableABIContextName(const std::string &Name) const { + return std::binary_search(UnstableABIContextNames.begin(), + UnstableABIContextNames.end(), Name); +} Index: lib/Driver/Tools.cpp =================================================================== --- lib/Driver/Tools.cpp +++ lib/Driver/Tools.cpp @@ -5646,6 +5646,18 @@ CmdArgs.push_back(I->getFilename()); } + // Add unstable C++ ABI flags. + if (Args.hasArg(options::OPT_funstable_cxx_abi_classes)) + CmdArgs.push_back("-funstable-c++-abi-classes"); + + for (const Arg *A : + Args.filtered(options::OPT_funstable_cxx_abi_classes_EQ)) { + CmdArgs.push_back(Args.MakeArgString( + std::string("-funstable-c++-abi-classes=") + A->getValue())); + CmdArgs.push_back( + Args.MakeArgString(std::string("-fdepfile-entry=") + A->getValue())); + } + // Finally add the compile command to the compilation. if (Args.hasArg(options::OPT__SLASH_fallback) && Output.getType() == types::TY_Object && Index: lib/Frontend/CompilerInvocation.cpp =================================================================== --- lib/Frontend/CompilerInvocation.cpp +++ lib/Frontend/CompilerInvocation.cpp @@ -1454,6 +1454,29 @@ return DefaultVisibility; } +static void parseUnstableABIContextNameFile(std::vector &Names, + StringRef Path, + DiagnosticsEngine &Diags) { + llvm::ErrorOr> FileOrErr = + llvm::MemoryBuffer::getFile(Path); + if (std::error_code EC = FileOrErr.getError()) { + Diags.Report(diag::err_fe_error_opening) << Path << EC.message(); + return; + } + + SmallVector Lines; + SplitString(FileOrErr.get()->getBuffer(), Lines, "\n\r"); + for (auto Line : Lines) { + auto Idx = Line.find('#'); + if (Idx != StringRef::npos) + Line = Line.substr(0, Idx); + + Line = Line.trim(); + if (!Line.empty()) + Names.push_back(Line); + } +} + static void ParseLangArgs(LangOptions &Opts, ArgList &Args, InputKind IK, const TargetOptions &TargetOpts, DiagnosticsEngine &Diags) { @@ -1929,6 +1952,23 @@ Opts.SanitizeAddressFieldPadding = getLastArgIntValue(Args, OPT_fsanitize_address_field_padding, 0, Diags); Opts.SanitizerBlacklistFiles = Args.getAllArgValues(OPT_fsanitize_blacklist); + + if (Args.hasArg(OPT_funstable_cxx_abi_classes)) + Opts.UnstableABIContextNames.push_back("**"); + + unsigned NumFiles = 0; + for (const Arg *A : Args.filtered(OPT_funstable_cxx_abi_classes_EQ)) { + ++NumFiles; + Opts.UnstableABIContextNamesPath = A->getValue(); + parseUnstableABIContextNameFile(Opts.UnstableABIContextNames, A->getValue(), + Diags); + } + + std::sort(Opts.UnstableABIContextNames.begin(), + Opts.UnstableABIContextNames.end()); + + if (NumFiles != 1) + Opts.UnstableABIContextNamesPath.clear(); } static void ParsePreprocessorArgs(PreprocessorOptions &Opts, ArgList &Args, Index: lib/Sema/Sema.cpp =================================================================== --- lib/Sema/Sema.cpp +++ lib/Sema/Sema.cpp @@ -128,6 +128,11 @@ // Initilization of data sharing attributes stack for OpenMP InitDataSharingAttributesStack(); + + if (LangOpts.isUnstableABIContextName("*")) + UnstableABIContexts.insert(ctxt.getTranslationUnitDecl()); + if (LangOpts.isUnstableABIContextName("**")) + UnstableABIGlobContexts.insert(ctxt.getTranslationUnitDecl()); } void Sema::addImplicitTypedef(StringRef Name, QualType T) { Index: lib/Sema/SemaDeclAttr.cpp =================================================================== --- lib/Sema/SemaDeclAttr.cpp +++ lib/Sema/SemaDeclAttr.cpp @@ -5480,6 +5480,12 @@ case AttributeList::AT_InternalLinkage: handleInternalLinkageAttr(S, D, Attr); break; + case AttributeList::AT_StableABI: + handleSimpleAttribute(S, D, Attr); + break; + case AttributeList::AT_UnstableABI: + handleSimpleAttribute(S, D, Attr); + break; // Microsoft attributes: case AttributeList::AT_MSNoVTable: Index: lib/Sema/SemaDeclCXX.cpp =================================================================== --- lib/Sema/SemaDeclCXX.cpp +++ lib/Sema/SemaDeclCXX.cpp @@ -4887,6 +4887,139 @@ } } +void Sema::checkClassABI(CXXRecordDecl *Record) { + // This can only be done accurately for non-dependent types, as the + // determination uses the class's bases, which may be dependent. + if (Record->isDependentType()) + return; + + // No need to do this for non-dynamic classes. + if (!Record->isDynamicClass()) + return; + + // First, see if this class inherits an ABI from a dynamic base class. If the + // bases disagree on which ABI to use, diagnose. + bool InheritsABI = false; + bool InheritedABIIsUnstable; + CXXRecordDecl *InheritedABIFrom; + + for (CXXBaseSpecifier &B : Record->bases()) { + auto Base = B.getType()->getAsCXXRecordDecl(); + // Base can be null in invalid programs (see PR16677). + if (!Base || !Base->isDynamicClass()) + continue; + if (InheritsABI) { + if (Base->isUnstableCXXABI() != InheritedABIIsUnstable) { + Diag(Record->getLocation(), diag::err_abi_mismatch) << Record; + CXXRecordDecl *Stable = + InheritedABIIsUnstable ? Base : InheritedABIFrom; + CXXRecordDecl *Unstable = + InheritedABIIsUnstable ? InheritedABIFrom : Base; + Diag(Stable->getLocation(), diag::note_stable_abi_base) << Stable; + Diag(Unstable->getLocation(), diag::note_unstable_abi_base) << Unstable; + } + } else { + InheritsABI = true; + InheritedABIIsUnstable = Base->isUnstableCXXABI(); + InheritedABIFrom = Base; + } + } + + bool HasStableAttr = Record->hasAttr(); + bool HasUnstableAttr = Record->hasAttr(); + if (HasStableAttr && HasUnstableAttr) { + Diag(Record->getLocation(), diag::err_abi_mismatch) << Record; + Diag(Record->getAttr()->getLocation(), + diag::note_stable_abi_attr); + Diag(Record->getAttr()->getLocation(), + diag::note_unstable_abi_attr); + } + + // If the class inherited an ABI, the inherited ABI must agree with the + // class's attributes. + if (HasStableAttr && InheritsABI && InheritedABIIsUnstable) { + Diag(Record->getLocation(), diag::err_abi_mismatch) << Record; + Diag(InheritedABIFrom->getLocation(), diag::note_unstable_abi_base) + << InheritedABIFrom; + Diag(Record->getAttr()->getLocation(), + diag::note_stable_abi_attr); + } + if (HasUnstableAttr && InheritsABI && !InheritedABIIsUnstable) { + Diag(Record->getLocation(), diag::err_abi_mismatch) << Record; + Diag(InheritedABIFrom->getLocation(), diag::note_stable_abi_base) + << InheritedABIFrom; + Diag(Record->getAttr()->getLocation(), + diag::note_unstable_abi_attr); + } + + if (HasStableAttr) { + return; + } else if (HasUnstableAttr) { + Record->setIsUnstableCXXABI(); + return; + } else if (InheritsABI) { + if (InheritedABIIsUnstable) + Record->setIsUnstableCXXABI(); + return; + } + + // This class's ABI is not inherited or based on an attribute. Infer it from + // context. + if (UnstableABIContexts.empty() && + UnstableABIGlobContexts.empty() && + Diags.isIgnored(diag::warn_cxx_stable_abi, Record->getLocation())) + return; + + // Find the innermost context not enclosed by an anonymous namespace, which + // is the context enclosing the outermost anonymous namespace. + DeclContext *NSContext = Record->getEnclosingNamespaceContext(); + DeclContext *OutermostAnonNS = nullptr; + DeclContext *DC = NSContext; + while (DC) { + if (DC->isNamespace() && cast(DC)->isAnonymousNamespace()) + OutermostAnonNS = DC; + DC = DC->getParent(); + } + DeclContext *InnermostExternalDC = NSContext; + if (OutermostAnonNS) + InnermostExternalDC = OutermostAnonNS->getParent(); + InnermostExternalDC = InnermostExternalDC->getPrimaryContext(); + + // Check if that context is in our set of contexts. + if (UnstableABIContexts.count(InnermostExternalDC)) { + Record->setIsUnstableCXXABI(); + return; + } + + // Check if that context or an enclosing context is in our set of glob + // contexts. + DC = InnermostExternalDC; + while (1) { + if (UnstableABIGlobContexts.count(DC)) { + Record->setIsUnstableCXXABI(); + return; + } + DC = DC->getParent(); + if (!DC) + break; + DC = DC->getPrimaryContext(); + } + + // Okay, we have inferred the stable ABI for this record. Warn if this is a + // non-system header. + if (!SourceMgr.isInSystemHeader(Record->getLocation())) { + Diag(Record->getLocation(), diag::warn_cxx_stable_abi) + << Record; + if (!getLangOpts().UnstableABIContextNamesPath.empty()) { + Diag(Record->getLocation(), diag::note_add_unstable_abi_file) + << InnermostExternalDC + << getLangOpts().UnstableABIContextNamesPath; + } else { + Diag(Record->getLocation(), diag::note_add_unstable_abi_attr); + } + } +} + /// \brief Perform semantic checks on a class definition that has been /// completing, introducing implicitly-declared members, checking for /// abstract types, etc. @@ -5026,6 +5159,8 @@ DeclareInheritingConstructors(Record); checkClassLevelDLLAttribute(Record); + + checkClassABI(Record); } /// Look up the special member function that would be called by a special @@ -7250,7 +7385,18 @@ StartLoc, Loc, II, PrevNS); if (IsInvalid) Namespc->setInvalidDecl(); - + + // See if this namespace should be added to the set of unstable ABI contexts. + if ((!PrevNS || IsStd) && II && + !getLangOpts().UnstableABIContextNames.empty()) { + NamespaceDecl *FirstNS = Namespc->getOriginalNamespace(); + std::string Name = Namespc->getQualifiedNameAsString(); + if (getLangOpts().isUnstableABIContextName(Name + "::*")) + UnstableABIContexts.insert(FirstNS); + else if (getLangOpts().isUnstableABIContextName(Name + "::**")) + UnstableABIGlobContexts.insert(FirstNS); + } + ProcessDeclAttributeList(DeclRegionScope, Namespc, AttrList); // FIXME: Should we be merging attributes? Index: test/Driver/funstable-cxx-abi.cpp =================================================================== --- /dev/null +++ test/Driver/funstable-cxx-abi.cpp @@ -0,0 +1,6 @@ +// RUN: %clang -funstable-c++-abi-classes -### %s 2>&1 | FileCheck -check-prefix=CLASSES %s +// CLASSES: "-funstable-c++-abi-classes" + +// RUN: %clang -funstable-c++-abi-classes=foo.txt -### %s 2>&1 | FileCheck -check-prefix=CLASSES-ARG %s +// CLASSES-ARG: "-funstable-c++-abi-classes=foo.txt" +// CLASSES-ARG: "-fdepfile-entry=foo.txt" Index: test/SemaCXX/Inputs/unstable-abi-list-global1.txt =================================================================== --- /dev/null +++ test/SemaCXX/Inputs/unstable-abi-list-global1.txt @@ -0,0 +1 @@ +* Index: test/SemaCXX/Inputs/unstable-abi-list-global2.txt =================================================================== --- /dev/null +++ test/SemaCXX/Inputs/unstable-abi-list-global2.txt @@ -0,0 +1 @@ +** Index: test/SemaCXX/Inputs/unstable-abi-list.txt =================================================================== --- /dev/null +++ test/SemaCXX/Inputs/unstable-abi-list.txt @@ -0,0 +1,2 @@ +unstable_glob::** +std::* Index: test/SemaCXX/unstable-cxx-abi-global1.cpp =================================================================== --- /dev/null +++ test/SemaCXX/unstable-cxx-abi-global1.cpp @@ -0,0 +1,15 @@ +// RUN: %clang_cc1 %s -std=c++11 -fsyntax-only -verify -funstable-c++-abi-classes=%S/Inputs/unstable-abi-list-global1.txt + +namespace foo { + +struct stable { // expected-note {{base 'stable' uses the stable ABI}} + virtual void f(); +}; + +} + +struct unstable { // expected-note {{base 'unstable' uses the unstable ABI}} + virtual void f(); +}; + +struct mixed_bases : foo::stable, unstable {}; // expected-error {{inconsistent ABI for class 'mixed_bases'}} Index: test/SemaCXX/unstable-cxx-abi-global2.cpp =================================================================== --- /dev/null +++ test/SemaCXX/unstable-cxx-abi-global2.cpp @@ -0,0 +1,21 @@ +// RUN: %clang_cc1 %s -std=c++11 -fsyntax-only -verify -funstable-c++-abi-classes +// RUN: %clang_cc1 %s -std=c++11 -fsyntax-only -verify -funstable-c++-abi-classes=%S/Inputs/unstable-abi-list-global2.txt + +namespace foo { + +struct unstable { // expected-note {{base 'unstable' uses the unstable ABI}} + virtual void f(); +}; + +} + +struct unstable { + virtual void f(); +}; + +struct [[clang::stable_abi]] stable { // expected-note {{base 'stable' uses the stable ABI}} + virtual void f(); +}; + +struct unstable_derived : foo::unstable, unstable {}; +struct mixed_bases : foo::unstable, stable {}; // expected-error {{inconsistent ABI for class 'mixed_bases'}} Index: test/SemaCXX/unstable-cxx-abi-warning.cpp =================================================================== --- /dev/null +++ test/SemaCXX/unstable-cxx-abi-warning.cpp @@ -0,0 +1,12 @@ +// RUN: %clang_cc1 %s -std=c++11 -fsyntax-only -verify -Wstable-c++-abi +// RUN: %clang_cc1 %s -std=c++11 -fsyntax-only -verify -Wstable-c++-abi -funstable-c++-abi-classes=%S/Inputs/unstable-abi-list.txt -funstable-c++-abi-classes=%S/Inputs/unstable-abi-list-global1.txt + +namespace ns { + +class non_dynamic {}; + +class dynamic { // expected-warning {{stable C++ ABI was inferred for class 'dynamic'}} expected-note {{add attribute clang::unstable_abi or clang::stable_abi to silence this warning}} + virtual void f(); +}; + +} Index: test/SemaCXX/unstable-cxx-abi.cpp =================================================================== --- /dev/null +++ test/SemaCXX/unstable-cxx-abi.cpp @@ -0,0 +1,91 @@ +// RUN: %clang_cc1 %s -std=c++11 -fsyntax-only -verify -funstable-c++-abi-classes=%S/Inputs/unstable-abi-list.txt -Wstable-c++-abi + +struct stable { // expected-note 3{{base 'stable' uses the stable ABI}} expected-warning {{stable C++ ABI was inferred for class 'stable'}} expected-note-re {{add the global namespace to file {{.*}} to silence this warning}} + virtual void f(); +}; +struct [[clang::unstable_abi]] unstable { // expected-note 3{{base 'unstable' uses the unstable ABI}} + virtual void f(); +}; + +struct +[[clang::stable_abi]] // expected-note {{stable ABI specified by attribute}} +[[clang::unstable_abi]] // expected-note {{unstable ABI specified by attribute}} +bistable { // expected-error {{inconsistent ABI for class 'bistable'}} + virtual void f(); +}; + +struct mixed_bases : stable, unstable {}; // expected-error {{inconsistent ABI for class 'mixed_bases'}} +struct mixed_base_vbase : stable, virtual unstable {}; // expected-error {{inconsistent ABI for class 'mixed_base_vbase'}} + +struct +[[clang::unstable_abi]] // expected-note {{unstable ABI specified by attribute}} +mixed_attr_base : stable { // expected-error {{inconsistent ABI for class 'mixed_attr_base'}} +}; + +struct +[[clang::stable_abi]] // expected-note {{stable ABI specified by attribute}} +mixed_attr_base2 : unstable { // expected-error {{inconsistent ABI for class 'mixed_attr_base2'}} +}; + +namespace std { + +struct unstable { + virtual void f(); +}; + +} + +namespace std { + +namespace { + +struct unstable2 { + virtual void f(); +}; + +namespace foo { + +struct unstable { + virtual void f(); +}; + +} + +} + +namespace bar { + +struct stable { // expected-warning {{stable C++ ABI was inferred for class 'stable'}} expected-note-re {{add namespace 'std::bar' to file {{.*}} to silence this warning}} + virtual void f(); +}; + +} + +struct non_dynamic { + struct unstable3 { + virtual void f(); + }; + void mf() { + struct unstable4 { + virtual void f(); + }; + struct unstable_derived : unstable, unstable4 {}; + } +}; + +void f() { + struct unstable5 { + virtual void f(); + }; + struct unstable_derived : unstable, unstable5 {}; +} + +} + +struct stable2 : std::non_dynamic { // expected-warning {{stable C++ ABI was inferred for class 'stable2'}} expected-note-re {{add the global namespace to file {{.*}} to silence this warning}} + virtual void f(); +}; + +struct unstable_derived : unstable, std::unstable2, std::foo::unstable, + std::non_dynamic::unstable3 {}; +struct stable_derived : stable, std::bar::stable, stable2 {};