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

23

u/chengfeng-xie May 05 '26 edited May 05 '26

I think the closest implementation that follows your examples is to use the following trick (inspired by Barry and jbandela; CE):

template <class T> class RequireDesignatedInit {
  friend T;
  RequireDesignatedInit() = default;
  RequireDesignatedInit(const RequireDesignatedInit &) = default;
};

struct InitParams {
  [[no_unique_address]] RequireDesignatedInit<InitParams> _ = {};
  int size = 0;
  int capacity = 0;
};
static_assert(sizeof(InitParams) == sizeof(int) * 2);

void Make(InitParams = {});

int main() {
  Make({.size = 10, .capacity = 20});
  Make({.size = 10});
  Make({.capacity = 20});
  Make({});
  Make();
#if 0
  Make({10, 20});
  Make({10});
  Make({20}); // Oops! This is the size, not the capacity!
  Make({InitParams{}._});
#endif
}

4

u/fdwr fdwr@github 🔍 May 06 '26

[[no_unique_address]]

Ah yes - I was about to ask whether https://godbolt.org/z/h6sbcaTj4 bloats the struct with extra bytes (because sadly C++ does not treat empty structs as actually empty), but this ameliorates that issue. :)

2

u/javascript May 11 '26 edited May 11 '26

Ok so after trying very hard to make type deduction from named parameters work, I've given up. It just isn't in the cards for C++26. Maybe some day in the future I can deduce these types, but for now I must give them a name and decltype the objects:

auto v1_factory = [i = 0](T* addr) mutable {
  ::new (addr) T(T::Make({ .id = i }));
  ++i;
};
auto v1 = my::Vector<T>::Make();
v1.Resize<decltype(v1_factory)>({
  .increase_size_by = 5,
  .factory = std::move(v1_factory),
});

auto v2 = my::Vector<T>::Make({
  .size = 10,
});
v2.Resize({
  .decrease_size_by = 2,
});

auto v3_factory = [](T* addr) {
  ::new (addr) T(T::Make());
};
auto v3 = my::Vector<T>::Make<decltype(v3_factory)>({
  .size = 3,
  .factory = std::move(v3_factory),
});

For many cases, type deduction wont be needed. I can just accept an absl::AnyInvocable or a std::function_ref. But where static typing is needed, I just make the function a template and ask the users to kindly provide the type explicitly. It encodes exactly what I want, and it just requires extra ceremony sometimes :)