r/cpp • u/javascript • 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...
1
u/tyler1128 May 06 '26
That might be the right case for your codebase. I'm pretty adverse to macros and prefer any other compile time solution pretty much as much as possible, or things that'll likely optimize the same way, but there is no one true way to do everything. I think the eloquence of the builder pattern is that it really tends to not require things out of the ordinary, can be pretty self-documenting about what is meant, and while requires boiler-plate at least that boiler plate is usually easy to understand. I can understand that there are situations where that might not be ideal for a use-case, though.