第十三章 auto作为模板参数占位符

从C++17开始,你可以使用占位符类型(autodecltype(auto)作为非类型模板参数。这意味着我们可以针对不同类型的非模板参数写泛型代码。

13.1 使用auto作为模板参数

从C++17开始,你可以使用auto来声明一个非类型模板参数。比如:

template<auto N> class S {
    ...
};

这允许我们针对不同类型都可以实例化非类型模板参数N:

S<42> s1; // OK: type of N in S is int
S<'a'> s2; // OK: type of N in S is char

然而,对于那些规则不允许的类型作为模板类型,这个特性仍然是没用的,即不会实例化成功:

S<2.5> s3; // ERROR: template parameter type still cannot be double

我们甚至在偏特化中可以写具体类型:

template<int N> class S<N> {
    ...
};

甚至支持类模板参数推导。比如:

template<typename T, auto N>
class A {
public:
    A(const std::array<T,N>&) {
    }
    A(T(&)[N]) {
    }
    ...
};

可以推导T的类型,N的类型,N的值。:

A a2{"hello"}; // OK, deduces A<const char, 6> with N being int

std::array<double,10> sa1;
A a1{sa1}; // OK, deduces A<double, 10> with N being std::size_t

你也可以修饰auto,比如,要求模板参数的类型是一个指针:

template<const auto* P> struct S;

使用可变参数模板,你可以参数化模板,使用一堆同构模板参数:

template<auto... VS> class HeteroValueList {
};

或者一堆异构模板参数:

template<auto V1, decltype(V1)... VS> class HomoValueList {
};

比如:

HeteroValueList<1, 2, 3> vals1; // OK
HeteroValueList<1, 'a', true> vals2; // OK
HomoValueList<1, 2, 3> vals3; // OK
HomoValueList<1, 'a', true> vals4; // ERROR

13.1.1 参数化模板以适用字符和字符串

使用该特性的一种应用是允许同时传入字符和字符串作为模板参数。比如我们可以使用折叠表达式输出任意数量的参数的个数:

#include <iostream>

template<auto Sep = ' ', typename First, typename... Args>
void print(const First& first, const Args&... args) {
    std::cout << first;
    auto outWithSep = [](const auto& arg) {
                        std::cout << Sep << arg;
                      };
    (... , outWithSep(args));
    std::cout << '\n';
}

使用空格作为模板参数的一个默认参数,我们可以输出带空格分隔的参数:

template<auto Sep = ' ', typename First, typename... Args>
void print (const First& firstarg, const Args&... args) {
    ...
}

我们仍然可以调用:

std::string s{"world"};
print(7.5, "hello", s); // prints: 7.5 hello world

但是有了参数化的print(),而且带分隔符Sep,我们现在可以显式的传递一个不同的字符作为第一个模板参数:

print<' '>(7.5, "hello", s); // prints: 7.5-hello-world

因为用了auto,我们甚至可以传一个字符串字面值,但这样的话就不得不声明一个没有链接(linkage)的对象:

static const char sep[] = ", ";
print<sep>(7.5, "hello", s); // prints: 7.5, hello, world

或者我们可以传一个分隔符,只要类型是允许作为模板参数的:

print<-11>(7.5, "hello", s); // prints: 7.5-11hello-11world

13.1.2 定义元编程常量

另一个auto这个特性的使用是更容易的定义编译时常量。你不必这样定义:

template<typename T, T v>
struct constant
{
    static constexpr T value = v;
};

using i = constant<int, 42>;
using c = constant<char, 'x'>;
using b = constant<bool, true>;

现在你可以这样子做:

template<auto v>
struct constant
{
    static constexpr auto value = v;
};

using i = constant<42>;
using c = constant<'x'>;
using b = constant<true>;

也不必:

template<typename T, T... Elements>
struct sequence {
};

using indexes = sequence<int, 0, 3, 4>;

可以这样:

template<auto... Elements>
struct sequence {
};

using indexes = sequence<0, 3, 4>;

现在你甚至可以定义编译时对象来代表一系列异构类型的值:(有点像condensed tuple(译注:这里没理解condensed tuple啥意思,所以保留原文))

using tuple = sequence<0, 'h', true>;

13.2 使用auto作为变量模板参数

你也可以使用auto作为带变量模板(不要被变量模板(variable template)所困扰,它指的是模板化的变量,并且是可变参数模板,即带有任意数量的参数)的模板参数。比如。下面的声明,可能在一个头文件里,它定义一个变量模板,这个模板是元素类型和元素个数已经被参数化后的:

template<typename T, auto N> std::array<T,N> arr;

在每个翻译单元中,所有使用arr<int,10>的地方都共享一个全局对象,而arr<long,10>arr<int,10u>将会是不同的全局对象。

下面的头文件将展示一个完整的例子:

#ifndef VARTMPLAUTO_HPP
#define VARTMPLAUTO_HPP

#include <array>

template<typename T, auto N> std::array<T,N> arr{};

void printArr();

#endif // VARTMPLAUTO_HPP

一个翻译单元可以修改这个变量模板的两个不同实例的值:

#include "vartmplauto.hpp"

int main()
{
    arr<int,5>[0] = 17;
    arr<int,5>[3] = 42;
    arr<int,5u>[1] = 11;
    arr<int,5u>[3] = 33;
    printArr();
}

另一个翻译单元可以打印这两个变量:

#include "vartmplauto.hpp"
#include <iostream>

void printArr()
{
    std::cout << "arr<int,5>: ";
    for (const auto& elem : arr<int,5>) {
        std::cout << elem << ' ';
    }
    std::cout << "\narr<int,5u>: ";
    for (const auto& elem : arr<int,5u>) {
        std::cout << elem << ' ';
    }
    std::cout << '\n';
}

输入结果如下:

arr<int,5>: 17 0 0 42 0
arr<int,5u>: 0 11 0 33 0

与之相同的方式,你可以声明一个常量,其类型是从初始值推导出来的:

template<auto N> constexpr auto val = N; // OK since C++17

然后后面使用它,比如,像下面一样:

auto v1 = val<5>; // v1 == 5, v1 is int
auto v2 = val<true>; // v2 == true, v2 is bool
auto v3 = val<'a'>; // v3 == ’a’, v3 is char

为了说明发生了什么,可以看看下面的例子:

std::is_same_v<decltype(val<5>), int> // yields false
std::is_same_v<decltype(val<5>), const int> // yields true
std::is_same_v<decltype(v1), int>; // yields true (because auto decays)

13.3 使用decltype(auto)作为模板参数

你可以使用另一种占位符类型,由C++14引入的decltype(auto)。注意,这个东西对于类型是如何推导的有非常特殊的规则。根据decltype的规则,如果传了个表达式而不是名字,它会根据表达式的值范畴(参见5.3)来推导类型:

  • prvalue的类型是type(比如临时变量)
  • lvalue的类型是type&(比如对象名字)
  • xvalue的类型是type&&(比如通过std::move()将对象转换为右值引用)

这意味着,你可以很容易的把参数模板推导成引用,其结果可能出乎意料:

#include <iostream>
template<decltype(auto) N>

struct S {
    void printN() const {
        std::cout << "N: " << N << '\n';
    }
};

static const int c = 42;
static int v = 42;

int main()
{
    S<c> s1; // deduces N as const int 42
    S<(c)> s2; // deduces N as const int& referring to c
    s1.printN();
    s2.printN();

    S<(v)> s3; // deduces N as int& referring to v
    v = 77;
    s3.printN(); // prints: N: 77
}

13.4 后记

对于费类型模板参数可以使用占位符类型首先由James Touton和Michael Spertus在https://wg21.link/n4469中作为其一部分提出。最后的公认措辞是由James Touton和Michael Spertus在https://wg21.link/p0127r2中给出。