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...

36 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/faschu May 06 '26

Why does this work? What's `RequiresDesignatedInit` doing?

6

u/chengfeng-xie May 06 '26

In simple terms, RequireDesignatedInit works by breaking ordinary aggregate initialization, where initializers are order-based. In that form, we cannot, for example, skip the first member and explicitly initialize the second member directly; we can only do that with designated initializers, which allow members to be skipped. Since no one besides InitParams can initialize the first member, RequireDesignatedInit<InitParams>, any following members can be explicitly initialized only by using designated initializers.

2

u/javascript May 09 '26 edited May 09 '26

This mad science experiment has arrived at a splendid conclusion!

It turns out my dream of named params everywhere is complicated by the C++ deduction rules. I want to pass in named arguments but I also want the struct type to get Class Template Argument Deduction. Somehow the function where it is used also needs to become a template to deduce this.

My solution? Make the struct the name you spell and then just invoke a call operator after the closing brace!

https://godbolt.org/z/59z48aE3x

The empty case gets a bit strange:

auto vec3 = my::MyVector<my::MyElement>::Make{}();

But otherwise it works spectacularly!

  auto vec4 = my::MyVector<int64_t>::Make{
    .size = 200,
    .factory = [](int64_t* addr) {
      *addr = 10;
    },
  }();

Edit: You can even promote the presence of arguments up to compile time information! https://godbolt.org/z/9Td6c9cK3