How I fell into a CTAD trap
CTAD (class template argument deduction) is a new c++17 feature that similarly to template functions, allows to automatically deduce class template arguments. It allows to simplify some of the metaprogramming code, since templates instantiation doesn’t have to be done with explicit types but it comes with a bit of caveats as well.
Copying vs wrapping
Considering a class template like i.e.
|
|
What should the type of second std::optional
be? There are two options:
(1) std::optional<int>
(2) std::optional<std::optional<int>>
In case of the former, the original type is unwrapped - this is so called ‘copying deduction’ case. In case of the latter, the type is taken as is, without any unwrapping - this is so called ‘wrapping deduction’ case.
Case (1) is the default behaviour. The compiler achieves that by implicitly declaring a function:
|
|
This is all cool but may lead to surprising unexpected results. In my case, I
wanted to design a functor system, which I can interchangeably compose to
define a more complex operations. I’ve chosen a templated types instead of
virtual inheritance and a type hierarchy because the latter would require
dynamic memory allocation and this is something I didn’t want to have. My
functors need to adhere to a simple contract - they need to declare a get
function like so:
|
|
Having that, I can declare a variety of functor types to form a more complex operations i.e.:
|
|
These two can be composed like so:
|
|
Which, as expected, will produce the result of 4
.
Surprisingly, the following will not work as expected:
|
|
This is, of course, due to the aforementioned ‘copy deduction’ rule. The type
of not_eight
can be checked using a function without definition:
|
|
The compilation will fail, revealing the type:
|
|
The type should be Double<Double<Constant<int>>>
but due to copy deduction,
which is preferential, the inner Double
has been unwrapped.
This can be fixed providing a user defined deduction guides to override the default behaviour:
|
|
Checking the type once again, I see the correct nested instantiation:
|
|
… and the functor behaves as expected:
std::cout << Double{Double{Constant{2}}}.get() << std::endl;
8
Conclusion
C++ is a complex language and it’s always better to have unit tests to avoid surprises like that. I’ve only scratched the surface in my post. I encourage reading through an excellent post on CTAD by vector of bool.