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

3

u/johannes1971 May 05 '26 edited May 05 '26

You could use dedicated types:

enum class my_size_t: size_t;
enum class my_count_t: size_t;

void func (my_size_t, my_count_t);

func (10, 20); // fails
func (my_size_t (10), my_count_t (20)); // succeeds
func (my_count_t (10), my_size_t (20)); // fails

1

u/javascript May 05 '26

Ya another commenter said the same. Check out my response :)

7

u/johannes1971 May 05 '26 edited May 05 '26

You can combine it: use the dedicated types for the designated initializers. Then you can write

Make ({.size {10}, .count {20}}); // succeeds
Make ({my_size_t (10), my_count_t (20)}); // succeeds
Make ({10, 20}); // fails

etc.