Ch15.5: Template Argument Deduction and CTAD
1. Who Writes the <...>
Nobody writes ::fast_io::io::println<int>(42). The
<int> is deduced from 42, and the same machinery
decides vector's element type
(Ch4.3) and pair's two types.
Deduction is not a convenience; it is what makes templates usable.
There are two questions to keep apart:
- Function templates: the arguments decide the template parameters, and the parameters decide the parameter types.
- Class templates: there is nothing to deduce from until C++17 invented CTAD (class template argument deduction), which projects constructor arguments onto the class template's parameters.
2. Deduction from a Function Call
For each parameter of the form T, T& or
T const&, the argument's type is taken apart and matched against the
pattern. Two rules do most of the work:
- Decay for by-value parameters.
Ttakes arrays and functions as pointers, dropsconst, and drops references. Somax("abc", "abd")withtemplate <typename T> T max(T, T)trieschar const *, not a string. - No decay for reference parameters.
T const&keeps the array:Tbecomeschar[N].
The second rule is how a function learns an array's length, which by-value parameters can never do:
template <typename T, ::std::size_t N>
[[nodiscard]] inline constexpr ::std::size_t array_size(T const (&)[N]) noexcept
{
return N;
}
3. Forwarding References: One Parameter, Two Meanings
T&& where T is deduced is not an rvalue reference;
it is a forwarding reference. Given an lvalue argument,
T deduces to U& and the parameter collapses to
U&; given an rvalue, T is U and the
parameter is U&&. The reference collapsing rule
(& && → &) does the work.
That makes ::std::is_lvalue_reference_v<T> a compile-time answer
to “was I handed a name or a temporary”, which is exactly what
::std::forward needs, and exactly what
::fast_io::parameter<T&> builds on in
Ch15.11:
template <typename T>
[[nodiscard]] inline constexpr ::std::string_view category(T &&) noexcept
{
if constexpr (::std::is_lvalue_reference_v<T>)
{
return "lvalue";
}
else
{
return "rvalue";
}
}
Watch the traps: T&& is a forwarding reference only when
T is deduced by that very function call. In
template <typename T> void f(::std::vector<T> &&)
the parameter is an ordinary rvalue reference, deducing from
vector<T>&& — a completely different set of rules.
4. When Deduction Cannot Work
Deduction only sees the argument types. If a parameter type does not contain a template parameter in a deducible position, nothing can be recovered:
template <typename T>
[[nodiscard]] inline T from_string(::std::string_view s) throws; // T is not deducible
// callers must say it: from_string<int>("42")
fast_io's ::fast_io::to<T>(...) is exactly this shape: the target
type is an explicit template argument because the input text cannot imply it. That is
why Ch2.13 writes
::fast_io::to<::std::uint_least64_t>("12") and never
::fast_io::to("12").
The other escape is a default template argument, which makes the explicit form optional rather than required:
template <typename T = ::std::string_view>
[[nodiscard]] inline T parse(::std::string_view s) throws;
5. CTAD and Deduction Guides
For a class template, the compiler looks at the constructor arguments and at every deduction guide in scope. Implicit guides are generated from the constructors; an explicit guide is a function declaration with no body whose return type names the specialisation:
template <typename T>
struct pair_like
{
T first{}, second{};
pair_like(T a, T b) noexcept : first{a}, second{b} {}
};
template <typename A, typename B>
pair_like(A, B) -> pair_like<::std::common_type_t<A, B>>;
Now pair_like p{1, 2.5} deduces pair_like<double>.
The implicit guide from the constructor would have failed — it demands one
T from two different types — and the explicit guide resolves the
conflict with ::std::common_type_t, which is the intent: “the
wider of the two argument types”.
One detail this compiler makes visible: ::std::float64_t is not required
to be double, and here it is not (same size, different type). So
decltype(p) is pair_like<double>, not
pair_like<::std::float64_t>. That is not a defect: the guide names
common_type_t, and the standard's answer for int and
double is double.
fast_io's containers ship guides of their own so that the allocator never has to be
written (Ch15.9 shows the real ones):
vector{1, 2, 3} deduces vector<int> with the default
allocator, and vector{::std::from_range, r} deduces the element type
from the range.
6. The Whole Program
#include <fast_io.h>
#include <cstddef>
#include <string_view>
#include <type_traits>
template <typename T>
[[nodiscard]] inline constexpr ::std::string_view category(T &&) noexcept
{
if constexpr (::std::is_lvalue_reference_v<T>)
{
return "lvalue";
}
else
{
return "rvalue";
}
}
template <typename T, ::std::size_t N>
[[nodiscard]] inline constexpr ::std::size_t array_size(T const (&)[N]) noexcept
{
return N;
}
template <typename T>
struct pair_like
{
T first{}, second{};
pair_like(T a, T b) noexcept : first{a}, second{b} {}
};
template <typename A, typename B>
pair_like(A, B) -> pair_like<::std::common_type_t<A, B>>;
int main()
{
try
{
int x{1};
::fast_io::io::println(category(x), " ", category(2));
int const arr[5]{};
::fast_io::io::println("array_size=", array_size(arr));
pair_like p{1, 2.5};
static_assert(::std::same_as<decltype(p), pair_like<double>>);
::fast_io::io::println("first=", p.first, " second=", p.second);
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
lvalue rvalue
array_size=5
first=1 second=2.5
7. Rules of Thumb
- Prefer
T const&for parameters you only read: it keeps arrays intact, avoids copies, and deduces exactly the argument's type. - Use
T&&only with::std::forward, and only when the function passes the argument on. A forwarding reference that is copied is a bug waiting for a move-only type. - Never deduce a type that can only be known by the caller —
to<T>takesTexplicitly, and that is correct. - Write a deduction guide as soon as the implicit one would be wrong or ambiguous; it is a two-line declaration and it documents the intent.
- CTAD does not reduce instantiations.
vector{i, j, k}andvector{a, b, c}are the same type, butvector{1, 2.5}would not be — deduction picks one element type, and the mixed call is ill-formed.