Contents

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.

1
2
std::optional<int> x{123};
std::optional y{x};

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:

1
2
template <typename T>
auto __deduce_optional(const std::optional<T>& t) -> std::optional<T>;

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template <typename T>
class Constant {
public:
    using value_type = T;

    explicit Constant(T t) :
        c{t}
    {
    }

    value_type get() const {
        return c;
    }
private:
    const T c;
};

Having that, I can declare a variety of functor types to form more complex operations i.e.:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
template <typename T>
class Double {
public:
    using value_type = typename T::value_type;

    explicit Double(T t) :
        t{t}
    {
    }

    value_type get() const {
        return 2 * t.get();
    }

private:
    T t;
};

These two can be composed like so:

1
auto four = Double{Constant{2}};

Which, as expected, will produce the result of 4.

Surprisingly, the following will not work as expected:

1
auto not_eight = Double{Double{Constant{2}}};

This is, of course, due to the aforementioned ‘copy deduction’ rule. The type of not_eight can be checked using a function without definition:

1
2
template <typename T>
void foo(T);

The compilation will fail, revealing the type:

1
ctad.cpp:(.text+0x68): undefined reference to `void foo<Double<Constant<int> > >(Double<Constant<int> >)'

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:

1
2
template <typename T>
Double(const Double<T>&) -> Double<Double<T>>;

Checking the type once again, I see the correct nested instantiation:

1
ctad.cpp:(.text+0x79): undefined reference to `void foo<Double<Double<Constant<int> > > >(Double<Double<Constant<int> > >)'

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