Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clang] Implement CWG2351 void{} #78060

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Conversation

MitalAshok
Copy link
Contributor

@MitalAshok MitalAshok commented Jan 13, 2024

Per CWG2351, allow void{}, treated the same as void(): a prvalue expression of type void that performs no initialization.

Note that the AST for the expression T{} looks like:

// using T = int;
CXXFunctionalCastExpr 'T':'int' functional cast to T <NoOp>
`-InitListExpr 'T':'int'
// using T = const int;
CXXFunctionalCastExpr 'int' functional cast to T <NoOp>
`-InitListExpr 'int'
// using T = void;
CXXFunctionalCastExpr 'T':'void' functional cast to T <ToVoid>
`-InitListExpr 'void'
// using T = const void;
CXXFunctionalCastExpr 'void' functional cast to T <ToVoid>
`-InitListExpr 'void'

As for void()/T() [T = const void], that looked like CXXScalarValueInitExpr 'void' and is unchanged after this.

For reference, C++98 [5.2.3p2] says:

The expression T(), where T is a simple-type-specifier (7.1.5.2) for a non-array complete object type or the (possibly cv-qualified) void type, creates an rvalue of the specified type, whose value is determined by default-initialization (8.5; no initialization is done for the void() case). [Note: if T is a non-class type that is cv-qualified, the cv-qualifiers are ignored when determining the type of the resulting rvalue (3.10). ]

Though it is a bit of a misnomer that, for T = void, CXXScalarValueInitExpr does not perform value initialization, it would be a breaking change to change the AST node for void(), so I simply reworded the doc comment.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Jan 13, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Jan 13, 2024

@llvm/pr-subscribers-clang

Author: Mital Ashok (MitalAshok)

Changes

As per CWG2351, allow void{}, treated the same as void(): a prvalue expression of type void that performs no initialization.

Note that the AST for the expression T{} looks like:

// using T = int;
CXXFunctionalCastExpr 'T':'int' functional cast to T &lt;NoOp&gt;
`-InitListExpr 'T':'int'
// using T = const int;
CXXFunctionalCastExpr 'int' functional cast to T &lt;NoOp&gt;
`-InitListExpr 'int'
// using T = void;
CXXFunctionalCastExpr 'void' functional cast to T &lt;NoOp&gt;
`-InitListExpr 'void'
// using T = const void;
CXXFunctionalCastExpr 'void' functional cast to T &lt;NoOp&gt;
`-InitListExpr 'void'

(Since the InitListExpr already has void before anything is done, the type doesn't need to be adjusted)

As for void()/T() [T = const void], that looked like CXXScalarValueInitExpr 'void' and is unchanged after this.

For reference, C++98 [5.2.3p2] says:

> The expression T(), where T is a simple-type-specifier (7.1.5.2) for a non-array complete object type or the (possibly cv-qualified) void type, creates an rvalue of the specified type, whose value is determined by default-initialization (8.5; no initialization is done for the void() case). [Note: if T is a non-class type that is cv-qualified, the cv-qualifiers are ignored when determining the type of the resulting rvalue (3.10). ]

Though it is a bit of a misnomer that, for T = void, CXXScalarValueInitExpr does not perform value initialization, it would be a breaking change to change the AST node for void(), so I simply reworded the doc comment.


Full diff: https://github.com/llvm/llvm-project/pull/78060.diff

5 Files Affected:

  • (modified) clang/docs/ReleaseNotes.rst (+2)
  • (modified) clang/include/clang/AST/ExprCXX.h (+3-2)
  • (modified) clang/lib/Sema/SemaExprCXX.cpp (+15-6)
  • (modified) clang/test/CXX/drs/dr23xx.cpp (+31)
  • (modified) clang/www/cxx_dr_status.html (+1-1)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 3cbce1be159437..00e200ea76e5bc 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -226,6 +226,8 @@ C++2c Feature Support
 
 Resolutions to C++ Defect Reports
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+- Implemented `CWG2351 <https://wg21.link/CWG2351>`_ which allows ``void{}``
+  as a prvalue of type ``void``.
 
 C Language Changes
 ------------------
diff --git a/clang/include/clang/AST/ExprCXX.h b/clang/include/clang/AST/ExprCXX.h
index 24278016431837..e7b732e4181127 100644
--- a/clang/include/clang/AST/ExprCXX.h
+++ b/clang/include/clang/AST/ExprCXX.h
@@ -2160,8 +2160,9 @@ class LambdaExpr final : public Expr,
   const_child_range children() const;
 };
 
-/// An expression "T()" which creates a value-initialized rvalue of type
-/// T, which is a non-class type.  See (C++98 [5.2.3p2]).
+/// An expression "T()" which creates an rvalue of type T, which is a
+/// non-class type. For non-void T, the rvalue is value-initialized.
+/// See (C++98 [5.2.3p2]).
 class CXXScalarValueInitExpr : public Expr {
   friend class ASTStmtReader;
 
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index 4ae04358d5df7c..1792ec6bb292d5 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -1600,12 +1600,21 @@ Sema::BuildCXXTypeConstructExpr(TypeSourceInfo *TInfo,
     return ExprError(Diag(TyBeginLoc, diag::err_init_for_function_type)
                        << Ty << FullRange);
 
-  // C++17 [expr.type.conv]p2:
-  //   If the type is cv void and the initializer is (), the expression is a
-  //   prvalue of the specified type that performs no initialization.
-  if (!Ty->isVoidType() &&
-      RequireCompleteType(TyBeginLoc, ElemTy,
-                          diag::err_invalid_incomplete_type_use, FullRange))
+  // C++17 [expr.type.conv]p2, per DR2351:
+  //   If the type is cv void and the initializer is () or {}, the expression is
+  //   a prvalue of the specified type that performs no initialization.
+  if (Ty->isVoidType()) {
+    if (Exprs.empty())
+      return new (Context) CXXScalarValueInitExpr(Context.VoidTy, TInfo,
+                                                  Kind.getRange().getEnd());
+    if (ListInitialization && cast<InitListExpr>(Exprs[0])->getNumInits() == 0)
+      return CXXFunctionalCastExpr::Create(
+          Context, Context.VoidTy, VK_PRValue, TInfo, CK_NoOp, Exprs[0],
+          /*Path=*/nullptr, CurFPFeatureOverrides(), Exprs[0]->getBeginLoc(),
+          Exprs[0]->getEndLoc());
+  } else if (RequireCompleteType(TyBeginLoc, ElemTy,
+                                 diag::err_invalid_incomplete_type_use,
+                                 FullRange))
     return ExprError();
 
   //   Otherwise, the expression is a prvalue of the specified type whose
diff --git a/clang/test/CXX/drs/dr23xx.cpp b/clang/test/CXX/drs/dr23xx.cpp
index d2f4e7652ab568..1bf30a80f743e5 100644
--- a/clang/test/CXX/drs/dr23xx.cpp
+++ b/clang/test/CXX/drs/dr23xx.cpp
@@ -69,6 +69,37 @@ namespace dr2346 { // dr2346: 11
   }
 }
 
+namespace dr2351 { // dr2351: 18
+#if __cplusplus >= 201103L
+  static_assert((void{}, true), "");
+  // since-cxx11-warning@-1 {{left operand of comma operator has no effect}}
+
+  void f() {
+    return void{};
+  }
+
+  template<typename T>
+  void g() {
+    return T{};
+  }
+  template void g<void>();
+
+  void h() {
+    return {};
+    // since-cxx11-error@-1 {{void function 'h' must not return a value}}
+  }
+
+  template<typename T, int... I>
+  T i() {
+    return T{I...};
+  }
+  template void i<void>();
+
+  static_assert((void({}), true), "");
+  // since-cxx11-error@-1 {{cannot initialize non-class type 'void' with a parenthesized initializer list}}
+#endif
+}
+
 namespace dr2352 { // dr2352: 10
   int **p;
   const int *const *const &f1() { return p; }
diff --git a/clang/www/cxx_dr_status.html b/clang/www/cxx_dr_status.html
index 4a3ed19161f9a5..207f032a5d45e4 100755
--- a/clang/www/cxx_dr_status.html
+++ b/clang/www/cxx_dr_status.html
@@ -13914,7 +13914,7 @@ <h2 id="cxxdr">C++ defect report implementation status</h2>
     <td><a href="https://cplusplus.github.io/CWG/issues/2351.html">2351</a></td>
     <td>CD5</td>
     <td><TT>void{}</TT></td>
-    <td class="unknown" align="center">Unknown</td>
+    <td class="unreleased" align="center">Clang 18</td>
   </tr>
   <tr id="2352">
     <td><a href="https://cplusplus.github.io/CWG/issues/2352.html">2352</a></td>

Copy link
Contributor

@Endilll Endilll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DR testing part looks fine.
I'm worried there are no regular tests. It's also not clear what happens in 98 mode. New code doesn't seem to care about language mode.

@Endilll Endilll requested a review from cor3ntin January 13, 2024 18:56
@MitalAshok
Copy link
Contributor Author

Initializer list syntax isn't available in C++98 mode (even as an extension? I can't find the option)
Even so, as a defect report it should apply to all prior C++ versions.

@Endilll
Copy link
Contributor

Endilll commented Jan 13, 2024

Initializer list syntax isn't available in C++98 mode (even as an extension? I can't find the option)

I'm not confident enough to properly review your changes, but my line of thinking is the following: void() is available in all language modes, but you're adding C++11-specific functionality (as uniform initializer syntax is not a thing in 98) without ever checking language mode. This sound suspicious enough to me to bring attention to, even if there are other reasons why it's not really necessary to check language mode here (which I'm not aware of, but my knowledge is limited).

Even so, as a defect report it should apply to all prior C++ versions.

Formally, defect reports apply to then latest publication, which is C++14, if not 17. Implementation indeed try to backport them as far back as they reasonably can. Start accepting uniform initialization syntax in 98 mode because of void{} DR doesn't sound reasonable to me.

@MitalAshok
Copy link
Contributor Author

Looking at other places, it looks like init-list stuff is guarded behind getLangOpts().CPlusPlus11, so I'll add that check.

It looks like this DR is CD5 (after C++17, applies to C++17), but void{} in C++11/14 without a warning seems fine.

As for "regular tests", do you mean in /clang/test/CXX? Should I also add tests there?

I also found this: https://stackoverflow.com/q/41131806 that void({}) apparently used to (erroneously) be accepted in Clang 3.8 but I can't find any tests for it

@Endilll
Copy link
Contributor

Endilll commented Jan 13, 2024

Looking at other places, it looks like init-list stuff is guarded behind getLangOpts().CPlusPlus11, so I'll add that check.

Corentin told me offline that check for list initialization that you do might be sufficient, as it can't pass in 98.

It looks like this DR is CD5 (after C++17, applies to C++17), but void{} in C++11/14 without a warning seems fine.

Yes, 11 and 14 are totally fine. We may want a pedantic warning, though.

As for "regular tests", do you mean in /clang/test/CXX? Should I also add tests there?

No, this is C++ conformance test suite. I meant clang/test/SemaCXX.

@cor3ntin
Copy link
Contributor

I would recommend adding a test in C++98 mode. I don't think gating is necessary, the code should not parse.

Adding a test for ({}) is also a good idea.

@Endilll
Copy link
Contributor

Endilll commented Jan 13, 2024

LGTM

Copy link
Contributor

@cor3ntin cor3ntin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Collaborator

@shafik shafik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick question

// C++17 [expr.type.conv]p2, per DR2351:
// If the type is cv void and the initializer is () or {}, the expression is
// a prvalue of the specified type that performs no initialization.
if (Ty->isVoidType()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to understand why the check is here, and I think it is b/c the diagnostic is coming from one of these two lines:

InitializationSequence InitSeq(*this, Entity, Kind, Exprs);
  ExprResult Result = InitSeq.Perform(*this, Entity, Kind, Exprs);

is that correct? So then we need to handle this case before we attempt the initialization sequence.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[expr.type.conv]p2 reads "T(expression-list) or T braced-init-list is a cast expression, a prvalue void, or direct-initialized from the initializer". So when we encounter void() or void{}, we shouldn't actually use initialization semantics.

Previously, void() worked because

// - If the initializer is (), the object is value-initialized.
if (Kind.getKind() == InitializationKind::IK_Value ||
(Kind.getKind() == InitializationKind::IK_Direct && Args.empty())) {
TryValueInitialization(S, Entity, Kind, *this);
return;
}
made it a value initialization by accident, and we allowed value initializing void

I've now added an assertion for void in TryValueInitialization because you shouldn't actually be able to get there, since there's no standard way to call for the value-intialization of void

@cor3ntin
Copy link
Contributor

cor3ntin commented Mar 7, 2024

@MitalAshok ping

@shafik
Copy link
Collaborator

shafik commented Apr 29, 2024

@MitalAshok ping

@zygoloid
Copy link
Collaborator

Note that the AST for the expression T{} looks like:

// using T = int;
CXXFunctionalCastExpr 'T':'int' functional cast to T <NoOp>
`-InitListExpr 'T':'int'
// using T = const int;
CXXFunctionalCastExpr 'int' functional cast to T <NoOp>
`-InitListExpr 'int'
// using T = void;
CXXFunctionalCastExpr 'void' functional cast to T <NoOp>
`-InitListExpr 'void'
// using T = const void;
CXXFunctionalCastExpr 'void' functional cast to T <NoOp>
`-InitListExpr 'void'

(Since the InitListExpr already has void before anything is done, the type doesn't need to be adjusted)

Nonetheless, I think it'd be more reasonable to use CK_ToVoid here rather than CK_NoOp. The void type for the InitListExpr is just a placeholder and not meant to mean that it's a real expression of type void. (We really ought to use a better placeholder there, so we can print the type out as <braced initializer list> instead of as void in diagnostics.)

@MitalAshok MitalAshok changed the title [SemaCXX] Implement CWG2351 void{} [Clang] Implement CWG2351 void{} May 21, 2024
@MitalAshok
Copy link
Contributor Author

@zygoloid The commit message was a bit outdated, it now takes the void type from T instead of the initializer list (this also preserves extra info like if it came from a typedef)

I've also changed it to CK_ToVoid

Copy link
Collaborator

@zygoloid zygoloid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants