From 61422f96653f1ceb01b0dd6229a8b06ac947e2f3 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 27 Sep 2019 20:24:36 +0000 Subject: [PATCH] For P0784R7: add support for explicit destructor calls and pseudo-destructor calls in constant evaluation. llvm-svn: 373122 --- clang/include/clang/Basic/DiagnosticASTKinds.td | 35 +++-- clang/lib/AST/ExprConstant.cpp | 128 ++++++++++++---- clang/lib/AST/Interp/State.h | 1 + clang/test/CXX/expr/expr.const/p2-0x.cpp | 2 +- clang/test/SemaCXX/constant-expression-cxx2a.cpp | 177 +++++++++++++++++++++++ 5 files changed, 298 insertions(+), 45 deletions(-) diff --git a/clang/include/clang/Basic/DiagnosticASTKinds.td b/clang/include/clang/Basic/DiagnosticASTKinds.td index 5e38b3a..7f935d4 100644 --- a/clang/include/clang/Basic/DiagnosticASTKinds.td +++ b/clang/include/clang/Basic/DiagnosticASTKinds.td @@ -38,7 +38,8 @@ def note_constexpr_pure_virtual_call : Note< "pure virtual function %q0 called">; def note_constexpr_polymorphic_unknown_dynamic_type : Note< "%select{|||||virtual function called on|dynamic_cast applied to|" - "typeid applied to}0 object '%1' whose dynamic type is not constant">; + "typeid applied to|destruction of}0 object '%1' whose dynamic type " + "is not constant">; def note_constexpr_dynamic_cast_to_reference_failed : Note< "reference dynamic_cast failed: %select{" "static type %1 of operand is a non-public base class of dynamic type %2|" @@ -120,11 +121,11 @@ def note_constexpr_this : Note< "evaluation of a call to a 'constexpr' member function">; def note_constexpr_lifetime_ended : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 " + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " "%select{temporary|variable}1 whose lifetime has ended">; def note_constexpr_access_uninit : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 " + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " "%select{object outside its lifetime|uninitialized object}1 " "is not allowed in a constant expression">; def note_constexpr_use_uninit_reference : Note< @@ -135,11 +136,11 @@ def note_constexpr_modify_const_type : Note< "in a constant expression">; def note_constexpr_access_volatile_type : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "|}0 " + "||}0 " "volatile-qualified type %1 is not allowed in a constant expression">; def note_constexpr_access_volatile_obj : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "|}0 " + "||}0 " "volatile %select{temporary|object %2|member %2}1 is not allowed in " "a constant expression">; def note_constexpr_volatile_here : Note< @@ -154,36 +155,36 @@ def note_constexpr_ltor_incomplete_type : Note< "read of incomplete type %0 is not allowed in a constant expression">; def note_constexpr_access_null : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 " + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " "dereferenced null pointer is not allowed in a constant expression">; def note_constexpr_access_past_end : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 " + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " "dereferenced one-past-the-end pointer is not allowed " "in a constant expression">; def note_constexpr_access_unsized_array : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 " + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " "element of array without known bound " "is not allowed in a constant expression">; def note_constexpr_access_inactive_union_member : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 " + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " "member %1 of union with %select{active member %3|no active member}2 " "is not allowed in a constant expression">; def note_constexpr_access_static_temporary : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 temporary " + "member call on|dynamic_cast of|typeid applied to|destruction of}0 temporary " "is not allowed in a constant expression outside the expression that " "created the temporary">; def note_constexpr_access_unreadable_object : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 object '%1' " - "whose value is not known">; + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " + "object '%1' whose value is not known">; def note_constexpr_access_deleted_object : Note< "%select{read of|read of|assignment to|increment of|decrement of|" - "member call on|dynamic_cast of|typeid applied to}0 heap allocated " - "object that has been deleted">; + "member call on|dynamic_cast of|typeid applied to|destruction of}0 " + "heap allocated object that has been deleted">; def note_constexpr_modify_global : Note< "a constant expression cannot modify an object that is visible outside " "that expression">; @@ -246,6 +247,12 @@ def note_constexpr_bit_cast_invalid_subtype : Note< def note_constexpr_bit_cast_indet_dest : Note< "indeterminate value can only initialize an object of type 'unsigned char'" "%select{, 'char',|}1 or 'std::byte'; %0 is invalid">; +def note_constexpr_pseudo_destructor : Note< + "pseudo-destructor call is not permitted in constant expressions " + "until C++20">; +def note_constexpr_destroy_complex_elem : Note< + "destruction of individual component of complex number is not yet supported " + "in constant expressions">; def note_constexpr_new : Note< "dynamic memory allocation is not permitted in constant expressions " "until C++20">; diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp index 12dc054..1b3ace0 100644 --- a/clang/lib/AST/ExprConstant.cpp +++ b/clang/lib/AST/ExprConstant.cpp @@ -613,9 +613,11 @@ namespace { }; } -static bool HandleDestructorCall(EvalInfo &Info, SourceLocation Loc, - APValue::LValueBase LVBase, APValue &Value, - QualType T); +static bool HandleDestruction(EvalInfo &Info, const Expr *E, + const LValue &This, QualType ThisType); +static bool HandleDestruction(EvalInfo &Info, SourceLocation Loc, + APValue::LValueBase LVBase, APValue &Value, + QualType T); namespace { /// A cleanup, and a flag indicating whether it is lifetime-extended. @@ -637,7 +639,7 @@ namespace { Loc = VD->getLocation(); else if (const Expr *E = Base.dyn_cast()) Loc = E->getExprLoc(); - return HandleDestructorCall(Info, Loc, Base, *Value.getPointer(), T); + return HandleDestruction(Info, Loc, Base, *Value.getPointer(), T); } *Value.getPointer() = APValue(); return true; @@ -1332,14 +1334,19 @@ static bool isModification(AccessKinds AK) { case AK_Assign: case AK_Increment: case AK_Decrement: + case AK_Destroy: return true; } llvm_unreachable("unknown access kind"); } +static bool isAnyAccess(AccessKinds AK) { + return isRead(AK) || isModification(AK); +} + /// Is this an access per the C++ definition? static bool isFormalAccess(AccessKinds AK) { - return isRead(AK) || isModification(AK); + return isAnyAccess(AK) && AK != AK_Destroy; } namespace { @@ -3174,6 +3181,10 @@ findSubobject(EvalInfo &Info, const Expr *E, const CompleteObject &Obj, const FieldDecl *UnionField = O->getUnionField(); if (!UnionField || UnionField->getCanonicalDecl() != Field->getCanonicalDecl()) { + // FIXME: If O->getUnionValue() is absent, report that there's no + // active union member rather than reporting the prior active union + // member. We'll need to fix nullptr_t to not use APValue() as its + // representation first. Info.FFDiag(E, diag::note_constexpr_access_inactive_union_member) << handler.AccessKind << Field << !UnionField << UnionField; return handler.failed(); @@ -3375,13 +3386,13 @@ static CompleteObject findCompleteObject(EvalInfo &Info, const Expr *E, } } - bool IsAccess = isFormalAccess(AK); + bool IsAccess = isAnyAccess(AK); // C++11 DR1311: An lvalue-to-rvalue conversion on a volatile-qualified type // is not a constant expression (even if the object is non-volatile). We also // apply this rule to C++98, in order to conform to the expected 'volatile' // semantics. - if (IsAccess && LValType.isVolatileQualified()) { + if (isFormalAccess(AK) && LValType.isVolatileQualified()) { if (Info.getLangOpts().CPlusPlus) Info.FFDiag(E, diag::note_constexpr_access_volatile_type) << AK << LValType; @@ -4840,9 +4851,13 @@ static bool checkDynamicType(EvalInfo &Info, const Expr *E, const LValue &This, /// Check that the pointee of the 'this' pointer in a member function call is /// either within its lifetime or in its period of construction or destruction. -static bool checkNonVirtualMemberCallThisPointer(EvalInfo &Info, const Expr *E, - const LValue &This) { - return checkDynamicType(Info, E, This, AK_MemberCall, false); +static bool +checkNonVirtualMemberCallThisPointer(EvalInfo &Info, const Expr *E, + const LValue &This, + const CXXMethodDecl *NamedMember) { + return checkDynamicType( + Info, E, This, + isa(NamedMember) ? AK_Destroy : AK_MemberCall, false); } struct DynamicType { @@ -4919,8 +4934,9 @@ static Optional ComputeDynamicType(EvalInfo &Info, const Expr *E, static const CXXMethodDecl *HandleVirtualDispatch( EvalInfo &Info, const Expr *E, LValue &This, const CXXMethodDecl *Found, llvm::SmallVectorImpl &CovariantAdjustmentPath) { - Optional DynType = - ComputeDynamicType(Info, E, This, AK_MemberCall); + Optional DynType = ComputeDynamicType( + Info, E, This, + isa(Found) ? AK_Destroy : AK_MemberCall); if (!DynType) return nullptr; @@ -5134,7 +5150,8 @@ struct StartLifetimeOfUnionMemberHandler { // * No variant members' lifetimes begin // * All scalar subobjects whose lifetimes begin have indeterminate values assert(SubobjType->isUnionType()); - if (!declaresSameEntity(Subobj.getUnionField(), Field)) + if (!declaresSameEntity(Subobj.getUnionField(), Field) || + !Subobj.getUnionValue().hasValue()) Subobj.setUnion(Field, getDefaultInitValue(Field->getType())); return true; } @@ -5571,9 +5588,9 @@ static bool HandleConstructorCall(const Expr *E, const LValue &This, Info, Result); } -static bool HandleDestructorCallImpl(EvalInfo &Info, SourceLocation CallLoc, - const LValue &This, APValue &Value, - QualType T) { +static bool HandleDestructionImpl(EvalInfo &Info, SourceLocation CallLoc, + const LValue &This, APValue &Value, + QualType T) { // Objects can only be destroyed while they're within their lifetimes. // FIXME: We have no representation for whether an object of type nullptr_t // is in its lifetime; it usually doesn't matter. Perhaps we should model it @@ -5609,7 +5626,7 @@ static bool HandleDestructorCallImpl(EvalInfo &Info, SourceLocation CallLoc, for (; Size != 0; --Size) { APValue &Elem = Value.getArrayInitializedElt(Size - 1); if (!HandleLValueArrayAdjustment(Info, &LocE, ElemLV, ElemT, -1) || - !HandleDestructorCallImpl(Info, CallLoc, ElemLV, Elem, ElemT)) + !HandleDestructionImpl(Info, CallLoc, ElemLV, Elem, ElemT)) return false; } @@ -5707,8 +5724,8 @@ static bool HandleDestructorCallImpl(EvalInfo &Info, SourceLocation CallLoc, return false; APValue *SubobjectValue = &Value.getStructField(FD->getFieldIndex()); - if (!HandleDestructorCallImpl(Info, CallLoc, Subobject, *SubobjectValue, - FD->getType())) + if (!HandleDestructionImpl(Info, CallLoc, Subobject, *SubobjectValue, + FD->getType())) return false; } @@ -5726,8 +5743,8 @@ static bool HandleDestructorCallImpl(EvalInfo &Info, SourceLocation CallLoc, return false; APValue *SubobjectValue = &Value.getStructBase(BasesLeft); - if (!HandleDestructorCallImpl(Info, CallLoc, Subobject, *SubobjectValue, - BaseType)) + if (!HandleDestructionImpl(Info, CallLoc, Subobject, *SubobjectValue, + BaseType)) return false; } assert(BasesLeft == 0 && "NumBases was wrong?"); @@ -5737,9 +5754,43 @@ static bool HandleDestructorCallImpl(EvalInfo &Info, SourceLocation CallLoc, return true; } -static bool HandleDestructorCall(EvalInfo &Info, SourceLocation Loc, - APValue::LValueBase LVBase, APValue &Value, - QualType T) { +namespace { +struct DestroyObjectHandler { + EvalInfo &Info; + const Expr *E; + const LValue &This; + const AccessKinds AccessKind; + + typedef bool result_type; + bool failed() { return false; } + bool found(APValue &Subobj, QualType SubobjType) { + return HandleDestructionImpl(Info, E->getExprLoc(), This, Subobj, + SubobjType); + } + bool found(APSInt &Value, QualType SubobjType) { + Info.FFDiag(E, diag::note_constexpr_destroy_complex_elem); + return false; + } + bool found(APFloat &Value, QualType SubobjType) { + Info.FFDiag(E, diag::note_constexpr_destroy_complex_elem); + return false; + } +}; +} + +/// Perform a destructor or pseudo-destructor call on the given object, which +/// might in general not be a complete object. +static bool HandleDestruction(EvalInfo &Info, const Expr *E, + const LValue &This, QualType ThisType) { + CompleteObject Obj = findCompleteObject(Info, E, AK_Destroy, This, ThisType); + DestroyObjectHandler Handler = {Info, E, This, AK_Destroy}; + return Obj && findSubobject(Info, E, Obj, This.Designator, Handler); +} + +/// Destroy and end the lifetime of the given complete object. +static bool HandleDestruction(EvalInfo &Info, SourceLocation Loc, + APValue::LValueBase LVBase, APValue &Value, + QualType T) { // If we've had an unmodeled side-effect, we can't rely on mutable state // (such as the object we're about to destroy) being correct. if (Info.EvalStatus.HasSideEffects) @@ -5747,7 +5798,7 @@ static bool HandleDestructorCall(EvalInfo &Info, SourceLocation Loc, LValue LV; LV.set({LVBase}); - return HandleDestructorCallImpl(Info, Loc, LV, Value, T); + return HandleDestructionImpl(Info, Loc, LV, Value, T); } //===----------------------------------------------------------------------===// @@ -6405,8 +6456,9 @@ public: // even though it's not quite the same thing. LValue CommonLV; if (!Evaluate(Info.CurrentCall->createTemporary( - E->getOpaqueValue(), getStorageType(Info.Ctx, E->getOpaqueValue()), - false, CommonLV), + E->getOpaqueValue(), + getStorageType(Info.Ctx, E->getOpaqueValue()), false, + CommonLV), Info, E->getCommon())) return false; @@ -6490,6 +6542,13 @@ public: if (!Member) return Error(Callee); This = &ThisVal; + } else if (const auto *PDE = dyn_cast(Callee)) { + if (!Info.getLangOpts().CPlusPlus2a) + Info.CCEDiag(PDE, diag::note_constexpr_pseudo_destructor); + // FIXME: If pseudo-destructor calls ever start ending the lifetime of + // their callee, we should start calling HandleDestruction here. + // For now, we just evaluate the object argument and discard it. + return EvaluateObjectArgument(Info, PDE->getBase(), ThisVal); } else return Error(Callee); FD = Member; @@ -6573,11 +6632,20 @@ public: return false; } else { // Check that the 'this' pointer points to an object of the right type. - if (!checkNonVirtualMemberCallThisPointer(Info, E, *This)) + // FIXME: If this is an assignment operator call, we may need to change + // the active union member before we check this. + if (!checkNonVirtualMemberCallThisPointer(Info, E, *This, NamedMember)) return false; } } + // Destructor calls are different enough that they have their own codepath. + if (auto *DD = dyn_cast(FD)) { + assert(This && "no 'this' pointer for destructor call"); + return HandleDestruction(Info, E, *This, + Info.Ctx.getRecordType(DD->getParent())); + } + const FunctionDecl *Definition = nullptr; Stmt *Body = FD->getBody(Definition); @@ -12798,8 +12866,8 @@ bool VoidExprEvaluator::VisitCXXDeleteExpr(const CXXDeleteExpr *E) { return false; } - if (!HandleDestructorCall(Info, E->getExprLoc(), Pointer.getLValueBase(), - (*Alloc)->Value, AllocType)) + if (!HandleDestruction(Info, E->getExprLoc(), Pointer.getLValueBase(), + (*Alloc)->Value, AllocType)) return false; if (!Info.HeapAllocs.erase(DA)) { diff --git a/clang/lib/AST/Interp/State.h b/clang/lib/AST/Interp/State.h index da19dd3..d027516 100644 --- a/clang/lib/AST/Interp/State.h +++ b/clang/lib/AST/Interp/State.h @@ -32,6 +32,7 @@ enum AccessKinds { AK_MemberCall, AK_DynamicCast, AK_TypeId, + AK_Destroy, }; // The order of this enum is important for diagnostics. diff --git a/clang/test/CXX/expr/expr.const/p2-0x.cpp b/clang/test/CXX/expr/expr.const/p2-0x.cpp index 63e3017..e847537 100644 --- a/clang/test/CXX/expr/expr.const/p2-0x.cpp +++ b/clang/test/CXX/expr/expr.const/p2-0x.cpp @@ -424,7 +424,7 @@ namespace PseudoDtor { int k; typedef int I; struct T { - int n : (k.~I(), 0); // expected-error {{constant expression}} + int n : (k.~I(), 1); // cxx11-warning {{constant expression}} cxx11-note {{pseudo-destructor}} }; } diff --git a/clang/test/SemaCXX/constant-expression-cxx2a.cpp b/clang/test/SemaCXX/constant-expression-cxx2a.cpp index 648e24f..152bfcf 100644 --- a/clang/test/SemaCXX/constant-expression-cxx2a.cpp +++ b/clang/test/SemaCXX/constant-expression-cxx2a.cpp @@ -1073,3 +1073,180 @@ namespace memory_leaks { constexpr bool h(UP p) { return *p; } static_assert(h({new bool(true)})); // ok } + +namespace dtor_call { + struct A { int n; }; + constexpr void f() { // expected-error {{never produces a constant expression}} + A a; // expected-note {{destroying object 'a' whose lifetime has already ended}} + a.~A(); + } + union U { A a; }; + constexpr void g() { + U u; + u.a.n = 3; + u.a.~A(); + // There's now effectively no active union member, but we model it as if + // 'a' is still the active union member (but its lifetime has ended). + u.a.n = 4; // Start lifetime of 'a' again. + u.a.~A(); + } + static_assert((g(), true)); + + constexpr bool pseudo() { + using T = bool; + bool b = false; + // This does evaluate the store to 'b'... + (b = true).~T(); + // ... but does not end the lifetime of the object. + return b; + } + static_assert(pseudo()); + + constexpr void use_after_destroy() { + A a; + a.~A(); + A b = a; // expected-note {{in call}} expected-note {{read of object outside its lifetime}} + } + static_assert((use_after_destroy(), true)); // expected-error {{}} expected-note {{in call}} + + constexpr void double_destroy() { + A a; + a.~A(); + a.~A(); // expected-note {{destruction of object outside its lifetime}} + } + static_assert((double_destroy(), true)); // expected-error {{}} expected-note {{in call}} + + struct X { char *p; constexpr ~X() { *p++ = 'X'; } }; + struct Y : X { int y; virtual constexpr ~Y() { *p++ = 'Y'; } }; + struct Z : Y { int z; constexpr ~Z() override { *p++ = 'Z'; } }; + union VU { + constexpr VU() : z() {} + constexpr ~VU() {} + Z z; + }; + + constexpr bool virt_dtor(int mode, const char *expected) { + char buff[4] = {}; + VU vu; + vu.z.p = buff; + switch (mode) { + case 0: + vu.z.~Z(); + break; + case 1: + ((Y&)vu.z).~Y(); + break; + case 2: + ((X&)vu.z).~X(); + break; + case 3: + ((Y&)vu.z).Y::~Y(); + vu.z.z = 1; // ok, still have a Z (with no Y base class!) + break; + case 4: + ((X&)vu.z).X::~X(); + vu.z.y = 1; // ok, still have a Z and a Y (with no X base class!) + break; + } + return __builtin_strcmp(expected, buff) == 0; + } + static_assert(virt_dtor(0, "ZYX")); + static_assert(virt_dtor(1, "ZYX")); + static_assert(virt_dtor(2, "X")); + static_assert(virt_dtor(3, "YX")); + static_assert(virt_dtor(4, "X")); + + constexpr void use_after_virt_destroy() { + char buff[4] = {}; + VU vu; + vu.z.p = buff; + ((Y&)vu.z).~Y(); + ((Z&)vu.z).z = 1; // expected-note {{assignment to object outside its lifetime}} + } + static_assert((use_after_virt_destroy(), true)); // expected-error {{}} expected-note {{in call}} + + constexpr void destroy_after_lifetime() { + A *p; + { + A a; + p = &a; + } + p->~A(); // expected-note {{destruction of object outside its lifetime}} + } + static_assert((destroy_after_lifetime(), true)); // expected-error {{}} expected-note {{in call}} + + constexpr void destroy_after_lifetime2() { + A *p = []{ A a; return &a; }(); // expected-warning {{}} expected-note {{declared here}} + p->~A(); // expected-note {{destruction of variable whose lifetime has ended}} + } + static_assert((destroy_after_lifetime2(), true)); // expected-error {{}} expected-note {{in call}} + + constexpr void destroy_after_lifetime3() { + A *p = []{ return &(A&)(A&&)A(); }(); // expected-warning {{}} expected-note {{temporary created here}} + p->~A(); // expected-note {{destruction of temporary whose lifetime has ended}} + } + static_assert((destroy_after_lifetime3(), true)); // expected-error {{}} expected-note {{in call}} + + constexpr void destroy_after_lifetime4() { // expected-error {{never produces a constant expression}} + A *p = new A; + delete p; + p->~A(); // expected-note {{destruction of heap allocated object that has been deleted}} + } + + struct Extern { constexpr ~Extern() {} } extern e; + constexpr void destroy_extern() { // expected-error {{never produces a constant expression}} + e.~Extern(); // expected-note {{cannot modify an object that is visible outside}} + } + + constexpr A &&a_ref = A(); // expected-note {{temporary created here}} + constexpr void destroy_extern_2() { // expected-error {{never produces a constant expression}} + a_ref.~A(); // expected-note {{destruction of temporary is not allowed in a constant expression outside the expression that created the temporary}} + } + + struct S { + constexpr S() { n = 1; } + constexpr ~S() { n = 0; } + int n; + }; + constexpr void destroy_volatile() { + volatile S s; + } + static_assert((destroy_volatile(), true)); // ok, not volatile during construction and destruction + + constexpr void destroy_null() { // expected-error {{never produces a constant expression}} + ((A*)nullptr)->~A(); // expected-note {{destruction of dereferenced null pointer}} + } + + constexpr void destroy_past_end() { // expected-error {{never produces a constant expression}} + A a; + (&a+1)->~A(); // expected-note {{destruction of dereferenced one-past-the-end pointer}} + } + + constexpr void destroy_past_end_array() { // expected-error {{never produces a constant expression}} + A a[2]; + a[2].~A(); // expected-note {{destruction of dereferenced one-past-the-end pointer}} + } + + union As { + A a, b; + }; + + constexpr void destroy_no_active() { // expected-error {{never produces a constant expression}} + As as; + as.b.~A(); // expected-note {{destruction of member 'b' of union with no active member}} + } + + constexpr void destroy_inactive() { // expected-error {{never produces a constant expression}} + As as; + as.a.n = 1; + as.b.~A(); // expected-note {{destruction of member 'b' of union with active member 'a'}} + } + + constexpr void destroy_no_active_2() { // expected-error {{never produces a constant expression}} + As as; + as.a.n = 1; + as.a.~A(); + // FIXME: This diagnostic is wrong; the union has no active member now. + as.b.~A(); // expected-note {{destruction of member 'b' of union with active member 'a'}} + } +} -- 2.7.4