Stay up-to-date with everything C++!
Content directly fetched from the subreddit just for you.
Join our group for discussions : @programminginc
Powered by : @r_channels
Post #25201
23
FYI: Class Template Argument Deduction (CTAD) does not compose well with Designated Initializers
I've been playing around with Designated Initializers lately to see if I can use them for robust NAMED function parameters. It seemed promising at first, but I've hit a wall when it comes to type deduction.
Instead of explaining all the steps, I'll just share where things ended up. So to define an API that uses designated initializers and CTAD, you end up with something like this:
struct NotProvided {};
template <
typename Size = NotProvided,
typename Capacity = NotProvided,
typename Factory = NotProvided
\>
struct Params {
Size size = {};
Capacity capacity = {};
Factory factory = {};
};
template <typename T>
struct Vector {
Vector(auto params) {
// ...pick apart the Params object...
}
};
You can and should make use of concepts to give more type information to the caller. Unconstrained templates are not as helpful when debugging, but I'm using them here to clarify the problem.
So given a callsite like this:
int64_t counter = 0;
auto v1 = Vector<int64_t>(Params{
.size = 0,
.capacity = 10,
.factory = [&\](int64_t* addr) {
::new (addr) int64_t(counter);
\++counter;
},
});
This should work fine and was what got me excited about the prospects of things. But once I started actually writing types like Vector and their tests, I kept getting surprising template error messages. One category was the deduction of reference types instead of underlying value types. That alone is infuriating but can be resolved with extra steps.
Instead, the deeper issue has to do with the interaction between CTAD and designated initializers. Consider this callsite:
auto v2 = Vector<int64_t>(Prams{
.size = 20,
.factory = DefaultFactoryFor<int64_t>,
});
This won't work. And the reason why is very upsetting.
When performing Class Template Argument Deduction, it first does an unevaluated invocation a synthesized overload set. That overload set, in this case, looks roughly like this:
Params();
Params(auto size);
Params(auto size, auto capacity);
Params(auto size, auto capacity, auto factory);
Then, when performing this invocation, it just uses the values of the arguments. It does not consider the field names.
So that means...
Params{.size = 10, .factory = DefaultFactoryFor<int64_t>}
... becomes ...
Params{10, DefaultFactory<int64_t>}
... which causes CTAD to select ...
Params(auto size, auto capacity);
Since the assumptions made about `auto capacity` are incompatible with the assumptions made about `.factory` arguments, this fails to compile!
I really wish that CTAD understood the field names when invoked via designated initializers.
I expected the default type values (`NotProvided`) and default member initializers (` = {}`) to work here. I thought it would unambiguously invoke the CTAD `Params(auto, auto, auto)` overload because all the arguments I did not spell (`.capacity`) would be "injected" into the callsite prior to type deduction. But that is not what happens.
If anyone knows a solution to this, I would be very happy.
https://redd.it/1te2yj1
@r_cpp
I've been playing around with Designated Initializers lately to see if I can use them for robust NAMED function parameters. It seemed promising at first, but I've hit a wall when it comes to type deduction.
Instead of explaining all the steps, I'll just share where things ended up. So to define an API that uses designated initializers and CTAD, you end up with something like this:
struct NotProvided {};
template <
typename Size = NotProvided,
typename Capacity = NotProvided,
typename Factory = NotProvided
\>
struct Params {
Size size = {};
Capacity capacity = {};
Factory factory = {};
};
template <typename T>
struct Vector {
Vector(auto params) {
// ...pick apart the Params object...
}
};
You can and should make use of concepts to give more type information to the caller. Unconstrained templates are not as helpful when debugging, but I'm using them here to clarify the problem.
So given a callsite like this:
int64_t counter = 0;
auto v1 = Vector<int64_t>(Params{
.size = 0,
.capacity = 10,
.factory = [&\](int64_t* addr) {
::new (addr) int64_t(counter);
\++counter;
},
});
This should work fine and was what got me excited about the prospects of things. But once I started actually writing types like Vector and their tests, I kept getting surprising template error messages. One category was the deduction of reference types instead of underlying value types. That alone is infuriating but can be resolved with extra steps.
Instead, the deeper issue has to do with the interaction between CTAD and designated initializers. Consider this callsite:
auto v2 = Vector<int64_t>(Prams{
.size = 20,
.factory = DefaultFactoryFor<int64_t>,
});
This won't work. And the reason why is very upsetting.
When performing Class Template Argument Deduction, it first does an unevaluated invocation a synthesized overload set. That overload set, in this case, looks roughly like this:
Params();
Params(auto size);
Params(auto size, auto capacity);
Params(auto size, auto capacity, auto factory);
Then, when performing this invocation, it just uses the values of the arguments. It does not consider the field names.
So that means...
Params{.size = 10, .factory = DefaultFactoryFor<int64_t>}
... becomes ...
Params{10, DefaultFactory<int64_t>}
... which causes CTAD to select ...
Params(auto size, auto capacity);
Since the assumptions made about `auto capacity` are incompatible with the assumptions made about `.factory` arguments, this fails to compile!
I really wish that CTAD understood the field names when invoked via designated initializers.
I expected the default type values (`NotProvided`) and default member initializers (` = {}`) to work here. I thought it would unambiguously invoke the CTAD `Params(auto, auto, auto)` overload because all the arguments I did not spell (`.capacity`) would be "injected" into the callsite prior to type deduction. But that is not what happens.
If anyone knows a solution to this, I would be very happy.
https://redd.it/1te2yj1
@r_cpp