TL;DR
- I’m working on P0960R3, which allows initializing aggregates with parenthesized list of values.
- I’ve implemented a subset of this featue. Before moving forward, I would like to ask for suggestions regarding the potential issues and future paths.
- The patches are here: D129531 and D129532.
Introduction
P0960R3 is a C++20 feature that allows initializing aggregates with parenthesis:
struct A {
int a;
double b;
};
void foo() {
A a(0, 1.9); // legal in C++20
int arr[](0, 1, 2, 3); // legal in C++20
}
It has sort of similarity to initializer list, with some differences. For example, narrowing conversion is not allowed in initializer list whilst legal in P0960R3:
struct A {
char a;
};
void foo() {
A a{1.1} // error !
A b(1.1) // legal but warning message.
}
You can refer to the document for more details.
Proposed method
At first I wanted to model this with CXXConstructExpr
, only to realize that this is not the best option. Aggregate, if I understand correctly, have only three constructor, namely default constructor, copy constructor and move contructor. Generating a constructor other than those three makes the aggregate “non-aggregate”, which is somewhat paradoxical and deviates from the semantics.
Here is my proposed method : a new kind of expression class, CXXParenListInitExpr
, is introduced. It is a simple class containing only an array of child expressions:
class CXXParenListInitExpr final
: public Expr,
private llvm::TrailingObjects<CXXParenListInitExpr, Expr *> {
...
By introducing this new expression can make the semantic analysis simple, since it can go into its own analysis function, VerifyOrPerformParenthesizedInitList
, instead of mixing up with the existing code path, making the overall logic clear and unstandable:
static bool VerifyOrPerformParenthesizedInitList(
Sema &S, const InitializedEntity &Entity, const InitializationKind &Kind,
ArrayRef<Expr *> Args, bool VerifyOnly, ExprResult *Result = nullptr) {
...
if (const ArrayType *AT =
S.getASTContext().getAsArrayType(Entity.getType())) {
...
} else if (isa<RecordType>(Entity.getType())) {
...
}
...
}
Advantages
- It does not violate the semantics.
- It keeps the semantic analysis simple since it does not mix up with the existing semantic analysis code path (for example, constructor semantic analysis).
Issues
diagnostic messages
Given this snippet:
struct A {
int a;
};
void foo() {
A a(1, 2, 4);
}
clang gives “no matching constructor” error whilst g++ gives a more precise error message
$ clang++ -std=c++20 -fsyntax-only test.cpp
test.cpp:8:5: error: no matching constructor for initialization of 'A'
A a(1, 2, 4);
^ ~~~~~~~
...
$ g++ -std=c++20 -fsyntax-only test.cpp
test.cpp:8:14: error: too many initializers for ‘A’
8 | A a(1, 2, 4);
| ^
We can give a better diagnostic message, but that would break some existing test cases. For example, this snippet in clang/test/SemaCXX/cxx2a-explicit-boo.cpp :
template<int a>
struct A {
explicit(1 << a)
A(int);
};
A<-1> a(0);
// expected-error@-1 {{no matching constructor}}
is diagnosed with a different message:
$ clang++ -std=c++20 test.cpp -fsyntax-only
test.cpp:10:7: error: excess elements in struct initializer
A<-1> a(0);
^ ~
...
I’m unsure whether this is acceptable. Therefore, I uploaded another patch: D129532 to focus on this issue.
Summary
We have two patches: D129531 and D129532. The first one contains the implementation; the second one is committed to improve the error messages.
Since I’m pretty young to C++ and clang/llvm development, any suggestion — even the one you consider trivial — is much appreciated.
Thanks for reading this