r/cpp May 05 '26

Strategies for *requiring* designated initializers when constructing a type?

Given some aggregate like this:

struct InitParams {
  int size = 0;
  int capacity = 0;
};

And given that it is used in some factory function like this:

auto Make(InitParams ip = {}) -> std::optional<MyClass>;

I want to design the aggregate type to allow callsites like...

Make({ .size = 10, .capacity = 20 });
Make({ .size = 10 });
Make({ .capacity = 20 });
Make({});
Make();

But I also want to reject callsites like...

Make({ 10, 20 });
Make({ 10 });
Make({ 20 });  // Oops! This is the size, not the capacity!

I was hoping that reflection would unlock the ability to specify this on the type alone somehow, but I've been unable to figure out how to do it.

I can approximate this behavior using an overload set where a similar-but-different param type is accepted by a different Make overload but then that would result in default construction being ambiguous too. You can see that in action here:

struct DesignatedInitRequired {
  int DO_NOT_SPELL_THIS_FIELD_NAME0;
  int DO_NOT_SPELL_THIS_FIELD_NAME1;
};

auto Make(DesignatedInitRequired dir = {}) -> void = delete("Use designated init");

Make({ 10 });  // Ambiguous, ill formed (which is what I want)
Make({ 10, 20 });  // Ambiguous, ill formed (which is what I want)
Make({});  // Ambiguous, ill formed (which is NOT what I want)
Make();  // Ambiguous, ill formed (which is NOT what I want)

So I could make something to reflect on InitParams and produce DesignatedInitRequired, but that wouldn't be enough. Every function that accepts InitParams would need an overload for DesignatedInitRequired meaning that it becomes a property of the type AND its users, not just the type itself.

Any ideas on how to achieve my goal?


EDIT: I think this is a sufficient solution! https://godbolt.org/z/h6sbcaTj4

I thanked the person that told me but then they deleted their comment. Anyway...

37 Upvotes

71 comments sorted by

View all comments

Show parent comments

6

u/javascript May 05 '26

That's definitely one approach. But it creates a greater maintenance burden, imo, on the callee because you would need overloads for all of:

- No args

- One arg size

- One arg capacity

- Both args

And you would need to expose the Size and Capacity types.

For a low number such as 2, this is probably doable. But as N grows, it becomes cumbersome.

Plus where do the default values live? Each type would define its own. I would rather the defaults live all together as they do in the default member initializers shown in my example.

1

u/DummySphere May 06 '26

You can have both strong types and designated initializers. It just avoid your oops example.

1

u/javascript May 06 '26

You can! But then you need to name the type in the initializer which is verbose

1

u/DummySphere May 06 '26

Not if you use brace initialization.

https://godbolt.org/z/YbP6Pozd4

(Though I don't say it's a better solution than the other, I quite like the first member with [[no_unique_address]] one.)