October 19, 2020

C++ 模板参数推导

实例化函数模板时, 模板实参必须是已知的, 但不必显式指定. 编译器会从函数模板的实参中推导缺失的模板实参. 另外模板参数推导也可以在 auto 说明符的上下文中从初始化器推导变量的类型.

函数模板参数的推导

对于下列的函数模板及其调用

template <typename T>
void f(P param);

// ...

f(A);

其中 P 和 T 有关, 例如 P 可能为 const T& 或 T&& 等. 有下列的推导规则:

  • A 的引用属性被忽略.
  • P 是非引用时, A 的 cv 限定符被忽略.
  • 如果 P 是无 cv 限定符的转发引用 (即 T&&), 且 A 是左值时, T 被推导为左值引用.
  • 如果 A 是数组或函数, P 是值时, 数组和函数退化为指针. P 是引用时, 不退化为指针.

这里 cv 限定符指的是 const 和 volatile.

使用下面的代码验证

#include <cstdio>

template <typename T>
void f(T param) {
    std::puts(__PRETTY_FUNCTION__);
}

int main() {
    int i = 0;
    f(i);
}

这个例子中 P = T, A = int. 结果是

void f(T) [T = int]

注意 __PRETTY_FUNCTION__ 是编译器扩展, 在 gcc 和 clang 中支持, 在 MSVC 中可以使用 __FUNCSIG__.

下面是一个经过实验的表格

PAT
Tintint
Tint*int*
Tint&int
Tconst intint
Tconst int *const int *
Tint * constint *
Tconst int &int
Tconst int * constconst int *
Tchar [2]char *
Tconst char [12]const char *
Tvoid (int)void (*)(int)
const Tintint
const Tint *int *
const Tint &int
const Tconst intint
const Tconst int *const int *
const Tconst int &int
const Tconst int * constconst int *
const Tchar [2]char *
const Tconst char [12]const char *
const Tvoid (int)void (*)(int)
T&intint
T&int *int *
T&int &int
T&const intconst int
T&const int *const int *
T&const int &const int
T&const int * constconst int * const
T&char [2]char [2]
T&const char [12]const char [12]
T&void (int)void (int)
T&&intint &
T&&int *int *&
T&&int &int &
T&&const intconst int &
T&&const int *const int *&
T&&const int &const int &
T&&const int * constconst int * const &
T&&char [2]char (&)[2]
T&&const char [12]const char (&)[12]
T&&void (int)void (&)(int)
T&&int &&int
const T&int &&int

auto 类型推导

C++11 中, 对于包含 auto 的变量声明, auto 被一个虚构的模板参数 U 替换, 实参 A 是初始化表达式, 按照模板参数推导的规则从 P 和 A 推导出 U 后, 将 U 代入 P 得到实际的变量类型.

例如

const auto& x = 1 + 2;

用 U 替换 auto, 得到

const U& x = 1 + 2;

这里 P 是 const U&, A 是 1 + 2, A 的类型为 int, 使用模板参数推导的规则, U 是 int, 因此变量 x 的类型为 const int &.

auto 类型推导基本与模板参数推导相同, 不同的一点是如果 A 是拷贝列表初始化, 则 U 被替换为 std::initializer_list<U>.

函数返回类型推导

C++14 中, 对于返回 auto 的函数, auto 被一个虚构的模板参数 U 替换, 参数 A 是 return 语句的表达式, 如果 return 语句没有操作数, A 为 void(). 使用上面的规则推导 U, 然后获得实际的返回类型.

例如

auto f() {
    return 42;
}

其中 A 是 42, 类型为 int. 因此 U 被推导为 int, 函数返回类型也为 int.

如果返回语句是 return; 或没有返回语句, A 被认为是 void(). 因此, 下面的函数

auto f() {
    return;
}

的返回类型为 void.

对于下面的函数

// C++14
#include <type_traits>

const auto f() {
    return 42;
}

static_assert(std::is_same<decltype(f()), const int>::value, "error");

如果按照模板参数推导的规则, 函数 f 的返回值应该是 const int. 但是上面的 static_cast 会失败:

error: static_assert failed due to requirement 'std::is_same<int, const int>::value' "error"

看起来 f 的返回类型实际上是 int. 这是由于非引用类型的函数返回值是纯右值, 而非类非数组的纯右值不能被 cv 限定, 因此虽然 f 的返回类型是 const int, 但它的 const 属性会被立刻剥离.

如果有多条 return 语句, 上述规则会对每条 return 语句执行, 如果它们的实际返回类型不同, 会引发编译错误.

类模板参数推导 (Class template argument deduction, CTAD)

C++17 将模板参数推导扩展到了仅给出类模板名称的对象构造. 例如

#include <type_traits>
#include <utility>

int main() {
    std::pair p{0, 0.0};
    static_assert(std::is_same_v<decltype(p), std::pair<int, double>>);
}

p 会被推导为 std::pair<int, double>. static_assert 验证了这点.

推导指引

如果构造函数是模板函数, CTAD 不会工作. 因此 C++17 也允许使用推导指引 (deduction guide). 它指示编译器如何从构造函数模板的参数中推导类模板参数.

#include <iterator>

// declaration of the template
template <typename T>
struct container {
    container(T t) {}
    template <typename Iter>
    container(Iter beg, Iter end);
};
// additional deduction guide
template <typename Iter>
container(Iter b, Iter e)
    -> container<typename std::iterator_traits<Iter>::value_type>;

注意推导指引不能放在类中构造函数的定义后面, 因为这会被认为是尾置返回类型继而引发编译错误.

References

Powered by Hugo & Kiss.