c++17 Class Template Argument Deduction(CTAD)

nxdong August 07, 2022 [cpp] #cpp

C++ How to Use Class Template Argument Deduction 部分学习和翻译

本文翻译自:how-to-use-class-template-argument-deduction

Class Template Argument Deduction (CTAD) 类模版参数推导是c++17的核心语言特性。

C++17的基础库也支持CTAD,升级之后可以在使用STL的的时候体验新特性,比如std::pair 或者 std::vector。

第三方库或者你自己的代码可以自动的从CTAD中获得便利,或者添加一点新代码(推断指引)来获取完整的好处。

幸运的是,使用CTAD或者提供推断指引是很容易的!

模版参数推导Template Argument Deduction

c++98 到c++14为函数模版提供了类型推导。

比如一个这样的函数模版:

template <typename RanIt> void sort(RanIt first, RanIt last);

你可以为std::vector<int> 排序而不用显式指定RanItstd::vector<int>::iterator

当编译器看到sort(v.begin(), v.end()); 时,它可以知道v.begin()v.end() 的类型

,所以他可以决定RanIt 的类型。

然而,类模版不能从这些规则获益。 如果想用两个int 构造一个std::pair, 一定要写成std::pair<int, int> p(11, 22); 这样,尽管编译器已经知道1122int 类型。

一个变通的方法是使用函数模版的参数类型推导:std::make_pair(11, 22) 返回 std::pair<int, int>. 跟大多数的权宜之计一样,这也有一些问题:

定义一个这样的辅助函数往往需要引入模版元编程(std::make_pair() 需要在其他的事情之外做完美转发与类型退化)。

降低了编译器的吞吐量(前端需要实例化,后端需要做优化)。

调试更复杂(需要跳进help函数)。

代码更冗杂(额外的 make_ 前缀,如果需要局部变量,需要用auto)。

Hello, CTAD World

C++17 在只用类模版名字构造对象的扩展了模版参数类型推断。

现在可以写std::pair(11, 22) ,这个与std::pair<int, int>(11, 22) 相同。

meow.cpp 文件内容如下:

#include <type_traits>
#include <utility>
int main() {
    std::pair p(1729, "taxicab");
    static_assert(std::is_same_v<decltype(p), std::pair<int, const char *>>);
}
// clang++ --std=c++17 meow.cpp 

CTAD在圆括号,大括号,有名变量,无名临时变量。

Another Example: array and greater

arr.cpp 内容如下:

#include <algorithm>
#include <array>
#include <functional>
#include <iostream>
#include <string_view>
#include <type_traits>
using namespace std;
int main() {
    array arr = { "lion"sv, "direwolf"sv, "stag"sv, "dragon"sv };
    static_assert(is_same_v<decltype(arr), array<string_view, 4>>);
    sort(arr.begin(), arr.end(), greater{});
    cout << arr.size() << ": ";
    for (const auto& e : arr) {
        cout << e << " ";
    }
    cout << "\n";
}
  1. CTAD 推断了std::array 的元素类型和数量。
  2. CTAD 也在模版默认参数中生效。greater{} 构造了一个greater<void>的对象,因为他的声明是template <typename T = void> struct greater;

自定义类型的CTAD

mypair.cpp 文件的内容:

#include <type_traits>
template <typename A, typename B> struct MyPair {
    MyPair() { }
    MyPair(const A&, const B&) { }
};
int main() {
    MyPair mp{11, 22};
    static_assert(std::is_same_v<decltype(mp), MyPair<int, int>>);
}

A 和 B 被推断为 int

but there are no constructor arguments and no default template arguments, so it can’t guess whether you want MyPair<int, int> or MyPair<Starship,Captain>.

Deduction Guides 推断指引

通常来说,CTAD在类模版的构造函数使用了所有类模版参数的时候自动生效。

然而,一些情况下构造函数自己就是模版,这个打断了CTAD所依赖的关系。

在这种情况下,类作者可以提供deduction guides 推导指引来告诉编译器如何从构造函数参数推断泪模版参数。

guides.cpp 文件内容如下:

#include <iterator>
#include <type_traits>
template <typename T>
struct MyVec
{
    template <typename Iter>
    MyVec(Iter, Iter) {}
};
template <typename Iter>
MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>;
template <typename A, typename B>
struct MyAdvancedPair
{
    template <typename T, typename U>
    MyAdvancedPair(T &&, U &&) {}
};
template <typename X, typename Y>
MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>;
int main()
{
    int *ptr = nullptr;
    MyVec v(ptr, ptr);
    static_assert(std::is_same_v<decltype(v), MyVec<int>>);
    MyAdvancedPair adv(1729, "taxicab");
    static_assert(std::is_same_v<decltype(adv), MyAdvancedPair<int, const char *>>);
}

// clang++ --std=c++17 ./guides.cpp 

有两个常用的 STL deduction guide 案例: 迭代器和完美转发。 terators and perfect forwarding。

迭代器类型

MyVec 类似使用T作元素模版的 std::vector , 但是它可以从迭代器类型 Iter

构造。

调用范围构造器提供了我们想要的类型信息,但是编译器不能确切的知道 IterT 之间的关系。

这就是deduction guide 起作用的地方。

在类模版定义之后,template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>; 语法告诉编译器:在为MyVec 运行 CTAD的时候,尝试为MyVec(Iter, Iter) 签名做模版参数推导。如果成功匹配,想要构造的类型就是MyVec<typename std::iterator_traits<Iter>::value_type> . 这个实际上就是从迭代器类型获取我们需要的类型。

完美转发

MyAdvancedPair 有一个跟 std::pair 一样的完美转发构造函数。编译器看ABTU 相比,是不同的类型,而且也不知道他们之间的关系。在这情况下,我们想实现的转换是不同的:我们需要类型退化(decay)。

有趣的是,我们不需要decay_t,尽管我们可以使用这个type trait(如果我们增加一些额外的代码)。

deduction guide template <typename X,typename Y> MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>; 是足够的。 这句话告诉编译器:在为MyAdvancedPair 运行 CTAD的时候,尝试为MyAdvancedPair(X, Y) 签名做模版参数推导,如果这个是值传递,推导做类型退化(decay)。 如果匹配成功,你想要构造的类型就是MyAdvancedPair<X, Y>

Enforcement

在一些极端情况,你可能希望deduction guides 拒绝一些代码。

enforce.cpp 内容如下:

#include <stddef.h>
#include <type_traits>
template <typename T, size_t N> struct MyArray {
    T m_array[N];
};
template <typename First, typename... Rest> struct EnforceSame {
    static_assert(std::conjunction_v<std::is_same<First, Rest>...>);
    using type = First;
};
template <typename First, typename... Rest> MyArray(First, Rest...)
    -> MyArray<typename EnforceSame<First, Rest...>::type, 1 + sizeof...(Rest)>;
int main() {
    MyArray a = { 11, 22, 33 };
    static_assert(std::is_same_v<decltype(a), MyArray<int, 3>>);
}
// clang++ --std=c++17 ./enforce.cpp

MyArray 的类型参数推导指引 MyArray(First, Rest...)强制所有的类型是相同的,并且推断出类型对象的数量。

Corner Cases for Experts: Non-Deduced Contexts

非推导类型:what-is-a-nondeduced-context

corner1.cpp 内容如下:

template <typename X> struct Identity {
    using type = X;
};
template <typename T> struct Corner1 {
    Corner1(typename Identity<T>::type, int) { }
};
int main() {
    Corner1 corner1(3.14, 1729);
}

typename Identity<T>::type 阻止编译器推导 Tdouble.

corner2.cpp 内容:

template <typename X> struct Identity {
    using type = X;
};
template <typename T> struct Corner2 {
    Corner2(T, long) { }
    Corner2(typename Identity<T>::type, unsigned long) { }
};
int main() {
    Corner2 corner2(3.14, 1729);
}

CTAD succeeds but constructor overload resolution fails. CTAD ignores the constructor taking (typename Identity<T>::type, unsigned long) due to the non-deduced context, so CTAD uses only (T, long) for deduction. Like any function template, comparing the parameters (T, long) to the argument types double, int deduces T to be double. (int is convertible to long, which is sufficient for template argument deduction; it doesn’t demand an exact match there.) After CTAD has determined that Corner2<double>should be constructed, constructor overload resolution considers both signatures (double,long) and (double, unsigned long) after substitution, and those are ambiguous for the argument types double, int (because int is convertible to both long and unsigned long, and the Standard doesn’t prefer either conversion).

Corner Cases for Experts: Deduction Guides Are Preferred

corner3.cpp 内容如下:

#include <type_traits>
template <typename T> struct Corner3 {
    Corner3(T) { }
    template <typename U> Corner3(U) { }
};
#ifdef WITH_GUIDE
    template <typename X> Corner3(X) -> Corner3<X *>;
#endif
int main() {
    Corner3 corner3(1729);
#ifdef WITH_GUIDE
    static_assert(std::is_same_v<decltype(corner3), Corner3<int *>>);
#else
    static_assert(std::is_same_v<decltype(corner3), Corner3<int>>);
#endif
}

参考

https://devblogs.microsoft.com/cppblog/how-to-use-class-template-argument-deduction/

https://stackoverflow.com/questions/25245453/what-is-a-nondeduced-context