forked from OSchip/llvm-project
[clang] p0388 conversion to incomplete array
This implements the new implicit conversion sequence to an incomplete (unbounded) array type. It is mostly Richard Smith's work, updated to trunk, testcases added and a few bugs fixed found in such testing. It is not a complete implementation of p0388. Differential Revision: https://reviews.llvm.org/D102645
This commit is contained in:
parent
a76cfc2e84
commit
dcd74716f9
|
@ -2535,8 +2535,10 @@ public:
|
|||
bool ObjCMethodsAreEqual(const ObjCMethodDecl *MethodDecl,
|
||||
const ObjCMethodDecl *MethodImp);
|
||||
|
||||
bool UnwrapSimilarTypes(QualType &T1, QualType &T2);
|
||||
void UnwrapSimilarArrayTypes(QualType &T1, QualType &T2);
|
||||
bool UnwrapSimilarTypes(QualType &T1, QualType &T2,
|
||||
bool AllowPiMismatch = true);
|
||||
void UnwrapSimilarArrayTypes(QualType &T1, QualType &T2,
|
||||
bool AllowPiMismatch = true);
|
||||
|
||||
/// Determine if two types are similar, according to the C++ rules. That is,
|
||||
/// determine if they are the same other than qualifiers on the initial
|
||||
|
|
|
@ -5844,7 +5844,11 @@ QualType ASTContext::getUnqualifiedArrayType(QualType type,
|
|||
/// Attempt to unwrap two types that may both be array types with the same bound
|
||||
/// (or both be array types of unknown bound) for the purpose of comparing the
|
||||
/// cv-decomposition of two types per C++ [conv.qual].
|
||||
void ASTContext::UnwrapSimilarArrayTypes(QualType &T1, QualType &T2) {
|
||||
///
|
||||
/// \param AllowPiMismatch Allow the Pi1 and Pi2 to differ as described in
|
||||
/// C++20 [conv.qual], if permitted by the current language mode.
|
||||
void ASTContext::UnwrapSimilarArrayTypes(QualType &T1, QualType &T2,
|
||||
bool AllowPiMismatch) {
|
||||
while (true) {
|
||||
auto *AT1 = getAsArrayType(T1);
|
||||
if (!AT1)
|
||||
|
@ -5856,12 +5860,21 @@ void ASTContext::UnwrapSimilarArrayTypes(QualType &T1, QualType &T2) {
|
|||
|
||||
// If we don't have two array types with the same constant bound nor two
|
||||
// incomplete array types, we've unwrapped everything we can.
|
||||
// C++20 also permits one type to be a constant array type and the other
|
||||
// to be an incomplete array type.
|
||||
// FIXME: Consider also unwrapping array of unknown bound and VLA.
|
||||
if (auto *CAT1 = dyn_cast<ConstantArrayType>(AT1)) {
|
||||
auto *CAT2 = dyn_cast<ConstantArrayType>(AT2);
|
||||
if (!CAT2 || CAT1->getSize() != CAT2->getSize())
|
||||
if (!(CAT2 && CAT1->getSize() == CAT2->getSize()) &&
|
||||
!(getLangOpts().CPlusPlus20 && AllowPiMismatch &&
|
||||
isa<IncompleteArrayType>(AT2)))
|
||||
return;
|
||||
} else if (!isa<IncompleteArrayType>(AT1) ||
|
||||
!isa<IncompleteArrayType>(AT2)) {
|
||||
} else if (isa<IncompleteArrayType>(AT1)) {
|
||||
if (!isa<IncompleteArrayType>(AT2) &&
|
||||
!(getLangOpts().CPlusPlus20 && AllowPiMismatch &&
|
||||
isa<ConstantArrayType>(AT2)))
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -5880,10 +5893,14 @@ void ASTContext::UnwrapSimilarArrayTypes(QualType &T1, QualType &T2) {
|
|||
/// "unwraps" pointer and pointer-to-member types to compare them at each
|
||||
/// level.
|
||||
///
|
||||
/// \param AllowPiMismatch Allow the Pi1 and Pi2 to differ as described in
|
||||
/// C++20 [conv.qual], if permitted by the current language mode.
|
||||
///
|
||||
/// \return \c true if a pointer type was unwrapped, \c false if we reached a
|
||||
/// pair of types that can't be unwrapped further.
|
||||
bool ASTContext::UnwrapSimilarTypes(QualType &T1, QualType &T2) {
|
||||
UnwrapSimilarArrayTypes(T1, T2);
|
||||
bool ASTContext::UnwrapSimilarTypes(QualType &T1, QualType &T2,
|
||||
bool AllowPiMismatch) {
|
||||
UnwrapSimilarArrayTypes(T1, T2, AllowPiMismatch);
|
||||
|
||||
const auto *T1PtrType = T1->getAs<PointerType>();
|
||||
const auto *T2PtrType = T2->getAs<PointerType>();
|
||||
|
@ -5944,7 +5961,7 @@ bool ASTContext::hasCvrSimilarType(QualType T1, QualType T2) {
|
|||
if (hasSameType(T1, T2))
|
||||
return true;
|
||||
|
||||
if (!UnwrapSimilarTypes(T1, T2))
|
||||
if (!UnwrapSimilarTypes(T1, T2, /*AllowPiMismatch*/ false))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4683,10 +4683,28 @@ LValue CodeGenFunction::EmitCastLValue(const CastExpr *E) {
|
|||
case CK_UserDefinedConversion:
|
||||
case CK_CPointerToObjCPointerCast:
|
||||
case CK_BlockPointerToObjCPointerCast:
|
||||
case CK_NoOp:
|
||||
case CK_LValueToRValue:
|
||||
return EmitLValue(E->getSubExpr());
|
||||
|
||||
case CK_NoOp: {
|
||||
// CK_NoOp can model a qualification conversion, which can remove an array
|
||||
// bound and change the IR type.
|
||||
// FIXME: Once pointee types are removed from IR, remove this.
|
||||
LValue LV = EmitLValue(E->getSubExpr());
|
||||
if (LV.isSimple()) {
|
||||
Address V = LV.getAddress(*this);
|
||||
if (V.isValid()) {
|
||||
llvm::Type *T =
|
||||
ConvertTypeForMem(E->getType())
|
||||
->getPointerTo(
|
||||
cast<llvm::PointerType>(V.getType())->getAddressSpace());
|
||||
if (V.getType() != T)
|
||||
LV.setAddress(Builder.CreateBitCast(V, T));
|
||||
}
|
||||
}
|
||||
return LV;
|
||||
}
|
||||
|
||||
case CK_UncheckedDerivedToBase:
|
||||
case CK_DerivedToBase: {
|
||||
const auto *DerivedClassTy =
|
||||
|
|
|
@ -2154,10 +2154,22 @@ Value *ScalarExprEmitter::VisitCastExpr(CastExpr *CE) {
|
|||
}
|
||||
case CK_AtomicToNonAtomic:
|
||||
case CK_NonAtomicToAtomic:
|
||||
case CK_NoOp:
|
||||
case CK_UserDefinedConversion:
|
||||
return Visit(const_cast<Expr*>(E));
|
||||
|
||||
case CK_NoOp: {
|
||||
llvm::Value *V = Visit(const_cast<Expr *>(E));
|
||||
if (V) {
|
||||
// CK_NoOp can model a pointer qualification conversion, which can remove
|
||||
// an array bound and change the IR type.
|
||||
// FIXME: Once pointee types are removed from IR, remove this.
|
||||
llvm::Type *T = ConvertType(DestTy);
|
||||
if (T != V->getType())
|
||||
V = Builder.CreateBitCast(V, T);
|
||||
}
|
||||
return V;
|
||||
}
|
||||
|
||||
case CK_BaseToDerived: {
|
||||
const CXXRecordDecl *DerivedClassDecl = DestTy->getPointeeCXXRecordDecl();
|
||||
assert(DerivedClassDecl && "BaseToDerived arg isn't a C++ object pointer!");
|
||||
|
|
|
@ -1555,6 +1555,12 @@ CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
|
|||
argCK = CK_AnyPointerToBlockPointerCast;
|
||||
} else if (ivarRef.getType()->isPointerType()) {
|
||||
argCK = CK_BitCast;
|
||||
} else if (argLoad.getType()->isAtomicType() &&
|
||||
!ivarRef.getType()->isAtomicType()) {
|
||||
argCK = CK_AtomicToNonAtomic;
|
||||
} else if (!argLoad.getType()->isAtomicType() &&
|
||||
ivarRef.getType()->isAtomicType()) {
|
||||
argCK = CK_NonAtomicToAtomic;
|
||||
}
|
||||
ImplicitCastExpr argCast(ImplicitCastExpr::OnStack, ivarRef.getType(), argCK,
|
||||
&argLoad, VK_PRValue, FPOptionsOverride());
|
||||
|
|
|
@ -1313,7 +1313,9 @@ static TryCastResult TryStaticCast(Sema &Self, ExprResult &SrcExpr,
|
|||
// lvalue-to-rvalue, array-to-pointer, function-to-pointer, and boolean
|
||||
// conversions, subject to further restrictions.
|
||||
// Also, C++ 5.2.9p1 forbids casting away constness, which makes reversal
|
||||
// of qualification conversions impossible.
|
||||
// of qualification conversions impossible. (In C++20, adding an array bound
|
||||
// would be the reverse of a qualification conversion, but adding permission
|
||||
// to add an array bound in a static_cast is a wording oversight.)
|
||||
// In the CStyle case, the earlier attempt to const_cast should have taken
|
||||
// care of reverse qualification conversions.
|
||||
|
||||
|
|
|
@ -6711,6 +6711,36 @@ QualType Sema::FindCompositePointerType(SourceLocation Loc,
|
|||
}
|
||||
|
||||
// FIXME: Can we unify the following with UnwrapSimilarTypes?
|
||||
|
||||
const ArrayType *Arr1, *Arr2;
|
||||
if ((Arr1 = Context.getAsArrayType(Composite1)) &&
|
||||
(Arr2 = Context.getAsArrayType(Composite2))) {
|
||||
auto *CAT1 = dyn_cast<ConstantArrayType>(Arr1);
|
||||
auto *CAT2 = dyn_cast<ConstantArrayType>(Arr2);
|
||||
if (CAT1 && CAT2 && CAT1->getSize() == CAT2->getSize()) {
|
||||
Composite1 = Arr1->getElementType();
|
||||
Composite2 = Arr2->getElementType();
|
||||
Steps.emplace_back(Step::Array, CAT1);
|
||||
continue;
|
||||
}
|
||||
bool IAT1 = isa<IncompleteArrayType>(Arr1);
|
||||
bool IAT2 = isa<IncompleteArrayType>(Arr2);
|
||||
if ((IAT1 && IAT2) ||
|
||||
(getLangOpts().CPlusPlus20 && (IAT1 != IAT2) &&
|
||||
((bool)CAT1 != (bool)CAT2) &&
|
||||
(Steps.empty() || Steps.back().K != Step::Array))) {
|
||||
// In C++20 onwards, we can unify an array of N T with an array of
|
||||
// a different or unknown bound. But we can't form an array whose
|
||||
// element type is an array of unknown bound by doing so.
|
||||
Composite1 = Arr1->getElementType();
|
||||
Composite2 = Arr2->getElementType();
|
||||
Steps.emplace_back(Step::Array);
|
||||
if (CAT1 || CAT2)
|
||||
NeedConstBefore = Steps.size();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const PointerType *Ptr1, *Ptr2;
|
||||
if ((Ptr1 = Composite1->getAs<PointerType>()) &&
|
||||
(Ptr2 = Composite2->getAs<PointerType>())) {
|
||||
|
@ -6771,8 +6801,6 @@ QualType Sema::FindCompositePointerType(SourceLocation Loc,
|
|||
continue;
|
||||
}
|
||||
|
||||
// FIXME: arrays
|
||||
|
||||
// FIXME: block pointer types?
|
||||
|
||||
// Cannot unwrap any more types.
|
||||
|
|
|
@ -3246,6 +3246,19 @@ static bool isQualificationConversionStep(QualType FromType, QualType ToType,
|
|||
!PreviousToQualsIncludeConst)
|
||||
return false;
|
||||
|
||||
// The following wording is from C++20, where the result of the conversion
|
||||
// is T3, not T2.
|
||||
// -- if [...] P1,i [...] is "array of unknown bound of", P3,i is
|
||||
// "array of unknown bound of"
|
||||
if (FromType->isIncompleteArrayType() && !ToType->isIncompleteArrayType())
|
||||
return false;
|
||||
|
||||
// -- if the resulting P3,i is different from P1,i [...], then const is
|
||||
// added to every cv 3_k for 0 < k < i.
|
||||
if (!CStyle && FromType->isConstantArrayType() &&
|
||||
ToType->isIncompleteArrayType() && !PreviousToQualsIncludeConst)
|
||||
return false;
|
||||
|
||||
// Keep track of whether all prior cv-qualifiers in the "to" type
|
||||
// include const.
|
||||
PreviousToQualsIncludeConst =
|
||||
|
@ -4199,12 +4212,15 @@ static ImplicitConversionSequence::CompareKind
|
|||
CompareQualificationConversions(Sema &S,
|
||||
const StandardConversionSequence& SCS1,
|
||||
const StandardConversionSequence& SCS2) {
|
||||
// C++ 13.3.3.2p3:
|
||||
// C++ [over.ics.rank]p3:
|
||||
// -- S1 and S2 differ only in their qualification conversion and
|
||||
// yield similar types T1 and T2 (C++ 4.4), respectively, and the
|
||||
// cv-qualification signature of type T1 is a proper subset of
|
||||
// the cv-qualification signature of type T2, and S1 is not the
|
||||
// yield similar types T1 and T2 (C++ 4.4), respectively, [...]
|
||||
// [C++98]
|
||||
// [...] and the cv-qualification signature of type T1 is a proper subset
|
||||
// of the cv-qualification signature of type T2, and S1 is not the
|
||||
// deprecated string literal array-to-pointer conversion (4.2).
|
||||
// [C++2a]
|
||||
// [...] where T1 can be converted to T2 by a qualification conversion.
|
||||
if (SCS1.First != SCS2.First || SCS1.Second != SCS2.Second ||
|
||||
SCS1.Third != SCS2.Third || SCS1.Third != ICK_Qualification)
|
||||
return ImplicitConversionSequence::Indistinguishable;
|
||||
|
@ -4225,79 +4241,35 @@ CompareQualificationConversions(Sema &S,
|
|||
if (UnqualT1 == UnqualT2)
|
||||
return ImplicitConversionSequence::Indistinguishable;
|
||||
|
||||
ImplicitConversionSequence::CompareKind Result
|
||||
= ImplicitConversionSequence::Indistinguishable;
|
||||
// Don't ever prefer a standard conversion sequence that uses the deprecated
|
||||
// string literal array to pointer conversion.
|
||||
bool CanPick1 = !SCS1.DeprecatedStringLiteralToCharPtr;
|
||||
bool CanPick2 = !SCS2.DeprecatedStringLiteralToCharPtr;
|
||||
|
||||
// Objective-C++ ARC:
|
||||
// Prefer qualification conversions not involving a change in lifetime
|
||||
// to qualification conversions that do not change lifetime.
|
||||
if (SCS1.QualificationIncludesObjCLifetime !=
|
||||
SCS2.QualificationIncludesObjCLifetime) {
|
||||
Result = SCS1.QualificationIncludesObjCLifetime
|
||||
? ImplicitConversionSequence::Worse
|
||||
: ImplicitConversionSequence::Better;
|
||||
}
|
||||
// to qualification conversions that do change lifetime.
|
||||
if (SCS1.QualificationIncludesObjCLifetime &&
|
||||
!SCS2.QualificationIncludesObjCLifetime)
|
||||
CanPick1 = false;
|
||||
if (SCS2.QualificationIncludesObjCLifetime &&
|
||||
!SCS1.QualificationIncludesObjCLifetime)
|
||||
CanPick2 = false;
|
||||
|
||||
while (S.Context.UnwrapSimilarTypes(T1, T2)) {
|
||||
// Within each iteration of the loop, we check the qualifiers to
|
||||
// determine if this still looks like a qualification
|
||||
// conversion. Then, if all is well, we unwrap one more level of
|
||||
// pointers or pointers-to-members and do it all again
|
||||
// until there are no more pointers or pointers-to-members left
|
||||
// to unwrap. This essentially mimics what
|
||||
// IsQualificationConversion does, but here we're checking for a
|
||||
// strict subset of qualifiers.
|
||||
if (T1.getQualifiers().withoutObjCLifetime() ==
|
||||
T2.getQualifiers().withoutObjCLifetime())
|
||||
// The qualifiers are the same, so this doesn't tell us anything
|
||||
// about how the sequences rank.
|
||||
// ObjC ownership quals are omitted above as they interfere with
|
||||
// the ARC overload rule.
|
||||
;
|
||||
else if (T2.isMoreQualifiedThan(T1)) {
|
||||
// T1 has fewer qualifiers, so it could be the better sequence.
|
||||
if (Result == ImplicitConversionSequence::Worse)
|
||||
// Neither has qualifiers that are a subset of the other's
|
||||
// qualifiers.
|
||||
return ImplicitConversionSequence::Indistinguishable;
|
||||
bool ObjCLifetimeConversion;
|
||||
if (CanPick1 &&
|
||||
!S.IsQualificationConversion(T1, T2, false, ObjCLifetimeConversion))
|
||||
CanPick1 = false;
|
||||
// FIXME: In Objective-C ARC, we can have qualification conversions in both
|
||||
// directions, so we can't short-cut this second check in general.
|
||||
if (CanPick2 &&
|
||||
!S.IsQualificationConversion(T2, T1, false, ObjCLifetimeConversion))
|
||||
CanPick2 = false;
|
||||
|
||||
Result = ImplicitConversionSequence::Better;
|
||||
} else if (T1.isMoreQualifiedThan(T2)) {
|
||||
// T2 has fewer qualifiers, so it could be the better sequence.
|
||||
if (Result == ImplicitConversionSequence::Better)
|
||||
// Neither has qualifiers that are a subset of the other's
|
||||
// qualifiers.
|
||||
return ImplicitConversionSequence::Indistinguishable;
|
||||
|
||||
Result = ImplicitConversionSequence::Worse;
|
||||
} else {
|
||||
// Qualifiers are disjoint.
|
||||
return ImplicitConversionSequence::Indistinguishable;
|
||||
}
|
||||
|
||||
// If the types after this point are equivalent, we're done.
|
||||
if (S.Context.hasSameUnqualifiedType(T1, T2))
|
||||
break;
|
||||
}
|
||||
|
||||
// Check that the winning standard conversion sequence isn't using
|
||||
// the deprecated string literal array to pointer conversion.
|
||||
switch (Result) {
|
||||
case ImplicitConversionSequence::Better:
|
||||
if (SCS1.DeprecatedStringLiteralToCharPtr)
|
||||
Result = ImplicitConversionSequence::Indistinguishable;
|
||||
break;
|
||||
|
||||
case ImplicitConversionSequence::Indistinguishable:
|
||||
break;
|
||||
|
||||
case ImplicitConversionSequence::Worse:
|
||||
if (SCS2.DeprecatedStringLiteralToCharPtr)
|
||||
Result = ImplicitConversionSequence::Indistinguishable;
|
||||
break;
|
||||
}
|
||||
|
||||
return Result;
|
||||
if (CanPick1 != CanPick2)
|
||||
return CanPick1 ? ImplicitConversionSequence::Better
|
||||
: ImplicitConversionSequence::Worse;
|
||||
return ImplicitConversionSequence::Indistinguishable;
|
||||
}
|
||||
|
||||
/// CompareDerivedToBaseConversions - Compares two standard conversion
|
||||
|
|
|
@ -373,10 +373,19 @@ namespace dr330 { // dr330: 7
|
|||
q = p; // ok
|
||||
q2 = p; // ok
|
||||
r = p; // expected-error {{incompatible}}
|
||||
s = p; // expected-error {{incompatible}} (for now)
|
||||
s = p;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2 {{incompatible}} (fixed by p0388)
|
||||
#endif
|
||||
t = p; // expected-error {{incompatible}}
|
||||
s = q; // expected-error {{incompatible}}
|
||||
s = q2; // expected-error {{incompatible}}
|
||||
s = q;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2 {{incompatible}} (fixed by p0388)
|
||||
#endif
|
||||
s = q2;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2 {{incompatible}} (fixed by p0388)
|
||||
#endif
|
||||
s = t; // ok, adding const
|
||||
t = s; // expected-error {{discards qualifiers}}
|
||||
(void) const_cast<P>(q);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// RUN: %clang_cc1 %s -triple %itanium_abi_triple -std=c++20 -emit-llvm -O2 -o - | FileCheck %s
|
||||
|
||||
// p0388 conversions to unbounded array
|
||||
// dcl.init.list/3
|
||||
|
||||
namespace One {
|
||||
int ga[1];
|
||||
|
||||
// CHECK-LABEL: @_ZN3One5frob1Ev
|
||||
// CHECK-NEXT: entry:
|
||||
// CHECK-NEXT: ret [0 x i32]* bitcast ([1 x i32]* @_ZN3One2gaE to [0 x i32]*)
|
||||
auto &frob1() {
|
||||
int(&r1)[] = ga;
|
||||
|
||||
return r1;
|
||||
}
|
||||
|
||||
// CHECK-LABEL: @_ZN3One5frob2ERA1_i
|
||||
// CHECK-NEXT: entry:
|
||||
// CHECK-NEXT: %0 = bitcast [1 x i32]* %arp to [0 x i32]*
|
||||
// CHECK-NEXT: ret [0 x i32]* %0
|
||||
auto &frob2(int (&arp)[1]) {
|
||||
int(&r2)[] = arp;
|
||||
|
||||
return r2;
|
||||
}
|
||||
} // namespace One
|
|
@ -0,0 +1,79 @@
|
|||
// RUN: %clang_cc1 -std=c++20 -verify %s
|
||||
// RUN: %clang_cc1 -std=c++17 -verify %s
|
||||
|
||||
// p0388 conversions to unbounded array
|
||||
// dcl.init.list/3
|
||||
|
||||
namespace One {
|
||||
int ga[1];
|
||||
|
||||
auto &frob1() {
|
||||
int(&r1)[] = ga;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2{{cannot bind to a value of unrelated type}}
|
||||
#endif
|
||||
|
||||
return r1;
|
||||
}
|
||||
|
||||
auto &frob2(int (&arp)[1]) {
|
||||
int(&r2)[] = arp;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2{{cannot bind to a value of unrelated type}}
|
||||
#endif
|
||||
|
||||
return r2;
|
||||
}
|
||||
} // namespace One
|
||||
|
||||
namespace Two {
|
||||
int ga[1];
|
||||
|
||||
auto *frob1() {
|
||||
int(*r1)[] = &ga;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2{{with an rvalue of type}}
|
||||
#endif
|
||||
|
||||
return r1;
|
||||
}
|
||||
|
||||
auto *frob2(int (*arp)[1]) {
|
||||
int(*r2)[] = arp;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2{{with an lvalue of type}}
|
||||
#endif
|
||||
|
||||
return r2;
|
||||
}
|
||||
} // namespace Two
|
||||
|
||||
namespace Four {
|
||||
using Inc = int[2];
|
||||
using Mat = Inc[1];
|
||||
Mat *ga[2];
|
||||
|
||||
auto *frob1() {
|
||||
Inc(*const(*r1)[])[] = &ga;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2{{with an rvalue of type}}
|
||||
#else
|
||||
// missing a required 'const'
|
||||
Inc(*(*r2)[])[] = &ga; // expected-error{{cannot initialize}}
|
||||
#endif
|
||||
|
||||
return r1;
|
||||
}
|
||||
|
||||
auto *frob2(Mat *(*arp)[1]) {
|
||||
Inc(*const(*r2)[])[] = arp;
|
||||
#if __cplusplus < 202002
|
||||
// expected-error@-2{{with an lvalue of type}}
|
||||
#else
|
||||
Inc(*(*r3)[])[] = arp; // expected-error{{cannot initialize}}
|
||||
#endif
|
||||
|
||||
return r2;
|
||||
}
|
||||
|
||||
} // namespace Four
|
Loading…
Reference in New Issue