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 meta programming 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. Functors within my library can be
interchangeably composed 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 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 this post. I encourage reading through an excellent post on CTAD by vector of bool.