[C++20][RFC] Suggestion desired regarding the implementation of P0960R3

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 :slight_smile:

2 Likes

I think the approach on the new expression node is the correct one. I would suggest seeing if you can just inherit it from InitListExpr instead, as that solves a lot of the existing problems that you’ll run into, as this aggregate init is basically JUST a braced-init-list, with different conversion rules.

As far as that change in diagnostic: I’m not a fan at all. I would think that this shouldn’t be considered in any case where we have a constructor (as that makes it not an aggregate) and should instead fall back to the ‘no matching constructor’ path.

Thanks for your reply :slight_smile:

That’s a great idea. I’ll look into it.

Do you mean we should fall back to the “no matching constructor” path even the struct/class type is an aggregate ?

Do you mean we should fall back to the “no matching constructor” path even the struct/class type is an aggregate ?

No, I am saying that record types with a constructor should say “no matching constructor”, and types without should give the ‘excess elements in struct initializer’. For the first ‘A’, it would be nice if we could get closer to G++, its worth the test churn.

BUT, the A<-1> looks to me like it isn’t an aggregate, right? So it shouldn’t change? Or am I missing a subtlety here?

I’m not sure whether this is an aggregate or not. But I see eye to eye with you that the diagnostic message shouldn’t change in this case. I’ll look into it.

Cf ⚙ D129531 [clang][C++20] P0960R3: Allow initializing aggregates from a parenthesized list of values - This comment should help you make progress, I hope

Yeah, that’s really helpful, Thanks !

I’ve addressed most of them. Concerning the error message, improving on it without breaking existing tests is not that trivial, so I may need more time to explore the solution.

FWIW, when I implemented this I found that the better strategy is not to set the sequence to failed (at least, for aggregates; for arrays setting it to failed is fine) but to check for aggregate parenthesized initialization within TryConstructorInitialization after the ResolveConstructorOverload steps. Then, if aggregate parenthesized initialization does not succeed, you can fall through to setting FK_ConstructorOverloadFailed and emit the diagnostic at the end of the OR_No_Viable_Function case of the FK_ConstructorOverloadFailed case of InitializationSequence::Diagnose, preserving all the existing diagnostics.

If you want to look at my implementation, it’s at 53627: p0960 Allow initializing aggregates from a parenthesized list of values by ecatmur · Pull Request #3 · ecatmur/llvm-project · GitHub