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:

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:

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