这是学习模板第二版的笔记和部分翻译。

函数模板

本章介绍函数模板,其是被参数化,这样它们可以表示一簇函数。

1.1 初识函数模板

函数模板提供函数行为,其可以被不同的类型所调用。换句话说,一个函数模板表示一簇函数。该表示看起来和普通函数很多相同,除了函数的一些元素尚未确定外:这些元素被参数化。下面可以看看一个简单的样例。

1.1.1定义模板

下面是返回两个值的最大值的函数模板:

template <typename T>
T max (T a, T  b)
{
    return b < a ? a : b;
}

该模板定义了这样一类函数,其返回两个值的较大值,这两个值是作为函数参数a和b传递的。参数的类型是左边的模板参数T。正如上面样例所示,模板参数使用下面的语法形式声明的:

template <comma-separated-list-of-parameters>

在样例中,参数列表是typename T。注意到<>被用作括号。关键字typename引入类型参数。这也是在C++模板中使用最多的种类,当然其他参数也是可行的,这将会在后续进行讨论。
这里,类型参数为T,当然你也可以使用任何标识符作为参数名称,但一般用T是习惯。类型参数表示任意类型,其会在调用者调用该函数时由调用者所决定。你可以使用任意类型(基本类型、类等)只要其提供模板所使用的运算。在样例中,类型T必须提供运算<,因为a和b使用该运算符进行比较。或许从max()定义中的并不明显的是类型T的值必须是可拷贝的,为了能够返回。
出于历史原因,你也可以使用关键字class,而不是typename来定义类型参数。关键字typename是在C++98标准稍后出来的。在这之前,class只是引入类型参数,这种方式就被保留下来。因此可以等价写成下面的形式:

template <class T>
T max(T a, T b)
{
    return b < a ? a : b;
}

在语义上,和前面的定义没有任何区别。所以即使这里使用了class,任何类型都可以用于模板参数。然而,因为使用class可能会有误导,你应该倾向于使用typename在这种上下文环境中。注意,这里不是类类型声明,这里无法使用关键字struct来取代typename,当声明类型参数时。

1.1.2使用模板

下面程序显示了怎么使用max()函数模板:

#include "max1.hpp"
#include <iostream>
#include <string>
int main()
{
int i = 42;
std::cout << "max(7,i):
" << ::max(7,i) << ’\n’;
double f1 = 3.4; double f2 = -6.7;
std::cout << "max(f1,f2):
" << ::max(f1,f2) << ’\n’;
std::string s1 = "mathematics"; std::string s2 = "math";
std::cout << "max(s1,s2):
" << ::max(s1,s2) << ’\n’;
}

在程序内,max()被调用三次:一次是两个ints,一次是两个double,最后一次是std::string。每次,都会计算最大值,作为程序的输出如下:
max(7,i): 42
max(f1,f2): 3.4
max(s1,s2): mathematics
注意到每次调用max()模板都是带着::限定符,这是为了保证在全局命名空间中能够找到我们的max模板,在STL中也有一个std::max()模板,在某些环境下可能会被调用或者导致二义性。
模板并不会编译成能够处理任何类型的单个实体。而是,对于每个类型都会从模板中生成不同的实体。因此,max()都会为三种类型进行编译。比如,max()的第一次调用 :

int i = 42;
... max(7,i) ...

该函数模板使用int作为模板参数T,这样,其具有下面代码的语义:

int max (int a, int b)
{
return b < a ? a : b;
}

由具体类型来取代模板参数的过程叫做实例化。其会导致一个模板的实例。
注意仅仅函数模板的使用就可以出发这样的实例化过程。这里无需程序员来单独请求实例化。
类似地,其他对max()的调用会实例化max模板为double和std::string,就好像它们已经声明和单独实现:

double max (double, double);
std::string max (std::string, std::string);

注意到void也是有效的模板参数,其提供的结果代码是有效的。比如:

template<typename T>
T foo(T*)
{
}
void* vp = nullptr;
foo(vp);             // OK: deduces void foo(void*)

1.1.3 两阶段转换

对于一个不支持模板内部的所有的运算符的类型的实例化尝试将会导致编译期错误。比如:

std::complex<float> c1, c2;
...
::max(c1,c2);
// doesn’t provide operator <
// ERROR at compile time

这样,模板在两个阶段被编译:

  1. 不是在定义时实例化,模板代码本身在忽略模板参数的情况下检查正确性,这包括:
  • 发现语法错误,比如缺少分号;
  • 使用并不依赖于模板参数的未知名称(类型名称、函数名称...),被发现;
  • 检查并不依赖于模板参数的静态断言;
  1. 在实例化时,会再次检查模板代码来确保所有代码都是有效的。即,现在尤其特殊,再次检查所有依赖于模板参数的部分;
    (总结:第一次检查就是定义模板代码时,假定模板参数相关都是对的,检查那些非模板参数相关的问题, 等到了模板实例化,会再次将模板参数相关的问题进行检验)。
    样例:
template<typename T>
void foo(T t)
{
undeclared();
// first-phase compile-time error if undeclared() unknown
undeclared(t);
// second-phase compile-time error if undeclared(T) unknown
static_assert(sizeof(int) > 10,
// always fails if sizeof(int)<=10
"int too small");
static_assert(sizeof(T) > 10,
//fails if instantiated for T with size <=10
"T too small");
}

这种检查的名称叫做两阶段查找,将会在本书的14章进行讨论。
注意到,一些编译器并不会在第一阶段执行所有的检查,所以你可能会在实例化阶段才能看到一般的错误。

编译和链接

两阶段转换会在实际中处理模板时导致一个重要的问题:当函数模板被以一种方式所使用,其会触发其实例化,编译器将(在一些点上)需要看见模板的定义。这会打破对于一般函数的编译和链接区别, 当函数的声明足以应付编译时的使用。处理该问题的方法在第9章讨论。对于当前,让我们采取最简单的办法:在头文件中实现每个模板。

1.2 模板参数推导

当我们用一些参数调用一个函数模板,比如max(),模板参数由我们所传递的参数决定。如果我们传递两个int作为参数类型,那么C++编译器推导处T必须是int。
然而, T可能只是类型的一部分。比如,如果我们使用常量引用来声明max():

template<typename T>
T max (T const& a, T const& b)
{
return b < a ? a : b;
}

传递int,T再次被推导为int,因为函数参数匹配成int const&。在类型推导期间,需要注意到自动类型转换有一些限制:

  • 当通过引用声明调用参数时,即使是普通的转换都不会应用类型推导。用相同模板参数T声明的两个参数必须准确匹配。
  • 当通过传值声明调用参数时,只有普通转换支持decay:带有const和volatile的限定符会被忽略,引用会转换为引用类型,原始的数组或函数转换为对应的指针类型。对于带有相同模板参数T的两个参数的decayed类型必须匹配。
template<typename T>
T max (T a, T b);
...
int const c = 42;
max(i, c);
// OK: T is deduced as int
max(c, c);
// OK: T is deduced as int
int& ir = i;
max(i, ir);
// OK: T is deduced as int
int arr[4];
foo(&i, arr);
// OK: T is deduced as int*

下面是错误的:

max(4, 7.2);
std::string s;
foo("hello", s);
// ERROR: T can be deduced as int or double
//ERROR: T can be deduced as char const[6] or std::string

这里有三种方式来处理这种错误:

  1. 强制转换为相匹配的类型:
max(static_cast<double>(4), 7.2);
  1. 显式指定T的类型,从而阻止编译器进行类型推导:
max<double>(4, 7. 2);   //OK
  1. 指定参数可能有不同的类型, 就是允许模板参数使用多个类型;

对于缺省参数的类型推导

注意到对于缺省调用参数,类型推导将不会起作用, 比如:

template <typename T>
void f(T = "");

f(1);     // OK: deduced T to be int, so that it calls f<int>(1)
f();      // ERROR: cannot deduce T

为了支持这种情况,你必须为模板参数声明一个默认参数,这将在1.4节进行讨论。

template<typename T = std::string>
void f(T = "");
...
f();       // OK

1.3 多个模板参数

正如我们目前所看到的那样,函数模板有两个不同的参数集合:

  1. 模板参数,其是在函数模板名称之前的尖括号中声明,比如:
template <typename T>   // T is template parameter;
  1. 调用参数,其是在函数模板名称后的parentheses中声明的:
T max (T a, T b)    // a and  b are call parameters;

你可能有许多模板参数。比如,你可以定义max()模板来给两个调用参数不同的类型:

template<typename T1, typename T2>
T1 max (T1 a, T2 b)
{
return b < a ? a : b; }
...
auto m = ::max(4, 7.2);             // OK, but type of first argument defines return type

其可能显式能够传递不同的类型给max()模板,但是,正如这个模板所示,其会产生一个问题。如果你使用一个参数类型作为返回类型,其他参数类型的参数可能会转换成这个类型,不管调用者的意图。这样,返回类型会取决于调用参数的熟悉怒。比如66.66和42的最大值京会是66.66,而44和66.66将会返回int 66。
C++提供不同的方法来解决该问题:

  • 引入第三个模板参数作为返回类型;
  • 让编译器找出返回类型;
  • 声明返回类型变为两个参数类型的"公共类型";
    接下来就是讨论这个选项。

1.3.1模板参数用于返回类型

我们之前的讨论显式模板参数推导使得我们可以调用函数模板和普通函数一样的语法来调用。我们并没有显式指定对应的模板参数。
然而,我们也提到我们可以显式指定模板参数的类型:

template<typename T>
T max (T a, T b);
...
::max<double>(4, 7.2);          // instantiate T as double

当模板参数和调用参数没有关联和模板参数无法被决定时这种情况下,你必须显式指定调用的模板参数。比如,你可以引入第三个模板参数类型来定义函数模板的返回类型:

template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b);

然而,模板参数推导并不会将返回类型考虑在内,RT并不会出现在函数调用参数的类型中,因此,RT无法被推导。
作为一个结果,你必须显式指定模板参数列表,比如:

template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b);
...
::max<int,double,double>(4, 7.2);       // OK, but tedious

到目前位置,我们已经查看了函数模板参数要么所有,要么都没有显式提及。另一个方法是显式指定第一个参数并让推导过程来派生余下的类型。一般而言,你必须指定所有的参数类型直到最后一个无法被隐式的参数类型。这样,如果你在我们的样例中变更了参数顺序,调用这只需要指定返回类型:

template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b);
...
::max<double>(4, 7.2)
//OK: return type is double, T1 and T2 are deduced

在这个样例中,对max的调用显式设置了RT为double,但是参数T1和T2都通过参数推导为int和double。
注意到这些对max()的修改版本并不会导致有显著优势。对于一参数的版本,你可以已经指定参数和返回类型,如果传递两个不同的类型参数。这样,保持简单是一个好主意,并使用max()的一参数版本。

1.3.2推导返回类型

如果返回类型取决于模板参数,最简单和最好的办法是推导返回类型让编译器来找。自从C++14,其可以通过简单而不用声明任何返回类型(你必须还需要用auto来声明返回类型):

template <typename T1, typename T2>
auto max(T1 a, T2 b)
{
    return b < a ? a : b;
}

事实上,对于返回类型使用auto而不用对应trailing返回类型(其会在尾部引入一个->)表明实际的返回类型必须从函数体中的返回语句来推导。当然,从函数体中推导返回类型必须是可行的。因此,该代码是可行的,并匹配多个返回语句。
在C++14之前,其只能通过或多或少让函数的实现是声明的一部分让编译器决定返回类型。在C++11我们可以从trailing返回类型语法中受益,允许我们使用调用参数。即,我们可以声明返回类型,这些返回类型是从运算符?:中派生产生的。

template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b<a?a:b)
{
return b < a ? a : b;
}

这里,结果类型是由运算符?:的规则所决定的,一般会产生一个非直观的期望结果。

template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b<a?a:b);

是一个声明,这样编译器使用运算符?:的规则调用给参数a和b来在编译期找出max()的返回类型。实现并没有必要匹配。实际上,在声明中使用true作为运算符 ? :的条件依然足够:

template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(true?a:b);

然而,在许多情况下这种定义有重大缺陷:其可能在返回类型是一个引用类型时发声,因为在某些情况下条件T必须是一个引用。出于这个原因,你必须返回从T decayed的类型,看起来如下:

#include <type_traits>
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> typename std::decay<decltype(true?a:b)>::type
{ return b < a ? a : b;
}

这里使用了类型特性std::decay<>,其会返回在成员type中返回结果类型。其是由在<type_trait>的标准库中定义的。因为成员type是一个类型,你必须用typename来限定表达式来访问它。
注意到类型auto的初始化总是decays。当返回类型也是auto时,也会作用到返回类型。auto作为返回类型表现如同下面的代码一样,这里a是一个由i, int decayed的类型所声明:

int i = 42;
int const& ir = i;    // ir refers to
i auto a = ir;      // a is declared as new object of type int
//PS:这句话,其实应该有错误,auto前面的i应该是笔误,否则无法通过gcc的编译;

1.3.3 返回类型作为公共类型

自从C++11,C++标准库提供了一种方法来指定选择更通用的类。std::common_type<>::type产生作为模板参数的两个或多个不同类型的"公共类型"。比如:

#include <type_traits>
template<typename T1, typename T2>
std::common_type_t<T1,T2> max (T1 a, T2 b)
{
return b < a ? a : b;
}

再次,std::common_type是一个类型特性,定义在<type_traits>,其会生成一个结构体,其中有type成员是表示结果类型的。这样,其核心使用如下:

typename std::common_type<T1,T2>::type
//since C++11

然而,自从C++14开始,你可以简化特性的使用,在特性名称后面加上_t,并跳过typename和::type,这样上面的返回类型定义简化为:

std::common_type_t<T1, T2>     //equivalent since C++14;

std::common_type<>的实现使用了一些模板编程的tricky,会在26.5中讨论。其会更觉运算符 ?:的语言规则或这特定类型的特化来选择返回类型。这样,::max(4, 7.2)和::max(7.2, 4)产生相同的结果double类型7.2。注意到std::common_type<>也是decays。

1.4缺省模板参数

你也可以为模板参数定义缺省值。这些值叫做默认模板参数并可以和任意种类的模板一起使用。其甚至可以引用到之前模板参数。
比如,如果你想要结合定义返回类型的方法和有多个参数类型的能力,你可以引入模板参数RT用于返回类型,默认作为两个参数的公共类型。再次,我们有多个选项:

  1. 直接使用运算符?:。然而,因为我们不得不使用运算符?:在调用参数a和b被声明之前,我们可以只使用其类型:
basics/maxdefault1.hpp
#include <type_traits>
template<typename T1, typename T2,
typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}

注意到std::decay_t<>的再次使用来确保没有返回引用。
也注意到该实现需要我们能够给传递的类型调用默认构造函数。这里有另一种解决方法,使用std::declval,然而,其会使得声明变得更加复杂。
2. 我们也可使用std::common_type<>类型特性来为返回类型指定默认值:

basics/maxdefault3.hpp
#include <type_traits>
template<typename T1, typename T2,
typename RT = std::common_type_t<T1,T2>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}

再次,注意到std::common_type<> decays这样返回值就不会变成一个引用。在所有情况下,作为一个调用者,你现在可以给返回类型使用默认值:

auto a = ::max(4, 7.2);

或者在所有其他参数类型显式后指定返回类型:

auto b = ::max<double,int,long double>(7.2, 4);

然而,再次我们遇到困难,我们必须指定三个类型从而能够指定返回类型。如果我们需要返回类型作为第一个模板参数的能力,剩余的将会从参数类型中推导。原则上,对于前导函数模板参数有默认参数即使参数没有下面的默认参数:

template<typename RT = long, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}

在这个定义下,比如,你可以这么调用:

int i; long l;
...
max(i, l);   // returns long (default argument of template parameter for return   type)
max<int>(4, 42);    // returns int as explicitly requested

如果这里对于模板参数是天然的缺省,那么该方法才是有意义的。这里我们需要,给模板参数的缺省参数依赖与之前模板参数。原则上,这是可能的,正如在26.5中讨论,但是该技术依赖于类型特性和复杂的定义。
基于上面的原因,最好的和最简单的方式是让编译器来推导,正如在1.3.2中所述一样。

1.5重载函数模板

和普通函数一样,函数模板可以被重载。即是,你可以用相同的函数名称,却有多个不同的函数定义,这样函数名称可以在函数调用中被使用,C++编译器必须决定调用的是哪一个候选者。该决定的规则可能会变得相当复杂,即使没有模板的情况下。在本节我们讨论当涉及到模板时的重载。如果你不太熟悉没有模板时的重载基本规则,可以参考附录C,这里我们提供了重载方案规则一个相对合理的细节。
下面介绍函数模板的重载:

basics/max2.cpp
// maximum of two int values:
int max (int a,
int b)
{
return b < a ? a : b;
}
// maximum of two values of any type:
template<typename T>
T max (T a, T b)
{
return b < a ? a : b;
}

int main()
{
::max(7, 42);   //calls the nontemplate for two ints
::max(7.0, 42.0);  // calls max<double> (by argument deduction)
::max(’a’, ’b’);   // calls max<char> (by argument deduction)
::max<>(7, 42);   //calls  max<int> (by argument deduction)
::max<double>(7, 42);  // calls max<double> (no argument deduction)
::max(’a’, 42.7);  // calls the nontemplate for two ints
}

正如这个例子所示,非模板函数可以与有相同名称的函数模板并可以实例化为相同的类型共存。所有其他因素都是相同的,重载解析过程相对于从模板中生成的函数会优先非模板函数。第一个调用就是遵循这个规则:

::max(7, 42);       // both int values match the nontemplate function perfectly

然而,当从模板中可以生成一个更加匹配的函数时,就会选择模板。这由max第二个例子和第三个调用就可以证实:

::max(7.0, 42.0);     // calls the max<double> (by argument deduction)
::max(’a’, ’b’);        //calls the max<char> (by argument deduction)

这里,模板是一个更好的匹配,因为没有要求从double或char到int的转换。
也可以显式的指定空的模板参数列表。该语法表示唯一的模板可以解析调用,但是所有的模板参数都可以从调用参数中推导得到:

::max<>(7, 42);         // calls max<int> (by argument deduction)

一个有趣的例子可重载max模板,能够显式只指定返回类型:

//basics/maxdefault4.hpp
template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}

现在,我们可以调用max,比如,如下:

auto a = ::max(4, 7.2);    // uses first template
auto b = ::max<long double>(7.2, 4);        // uses second template

然而,当调用:

auto c = ::max<int>(4, 7.2);
// ERROR: both function templates match

两个模板都可以匹配,这会导致重载方案过程无法选择出更好的,会导致二义性错误。这样,当重载函数模板时,你应该确保它们只能后其中一个匹配上。
对于为指针和普通C字符串的重载max模板的有用例子是:

//basics/max3val.cpp
#include <cstring>
#include <string>

// maximum of two values of any type:
template<typename T>
T max (T a, T b)
{
    return b < a ? a : b;
}
// maximum of two pointers:
template<typename T>
T* max (T* a, T* b)
{
    return  *b < *a ? a : b;
}
// maximum of two C-strings:
char const* max (char   const* a,   char const* b)
{
    return  std::strcmp(b,a) < 0   ? a : b;
}

int main ()
{
    int a = 7;
    int b = 42;
    auto m1 = ::max(a,b); // max() for two values of type int
    std::string s1 ="hey"; 
    std::string s2 = "you"; 
    auto m2 = ::max(s1,s2); // max() for two values of type std::string
    int* p1 = &b;
    int* p2 = &a;
    auto m3 = ::max(p1,p2); // max() for two pointers
    char const* x = "hello"; 
    char const* y = "world"; 
    auto m4 = ::max(x,y);   // max() for two C-strings
}

注意到max()所有重载中我们通过值传递。一般而言,在重载函数模板时除了必须改变外,最好是不要修改。你应该限制你的变更参数的数目或显式的指定模板参数。否则,就会产生意料之外的影响。比如,如果你实现max()模板通过引用来传递,并在两个C-string的重载版本中值传递,你将无法使用三个参数的版本来计算三个C-string的最大值:

#include <cstring>
// maximum of two values of any type (call-by-reference)
template<typename T> 
T const& max (T const& a, T const& b)
{
        return b < a ? a : b;
}
// maximum of two C-strings (call-by-value)
char const* max (char const* a, char const* b)
{
    return std::strcmp(b,a) < 0 ? a : b;
}
// maximum of three values of any type (call-by-reference)
template<typename T>
T const& max (T const& a, T const& b, T const& c)
{
    return max (max(a,b), c);
// error if max(a,b) uses call-by-value
}
int main ()
{
auto m1 = ::max(7, 42, 68);
// OK
char const* s1 = "frederic";
char const* s2 = "anica";
char const* s3 = "lucas";
auto m2 = ::max(s1, s2, s3);
//run-time ERROR
}

问题在于,如果你为三个C-string调用max(),该语句:

return max(max(a, b), c);

将会变得运行时错误,因为C-string,max(a, b)会创建一个新的、临时的局部变量,其会在返回时返回引用,但是该临时变量值在返回语句返回时就已经消亡,使得main()中是一个空引用。不幸的是,该错误是如此脆弱,并不会在所有情况中出现。
相反第,注意到,在main中的第一个max调用并没有遇到这样的问题。会为(7.42, 68)参数创建临时变量,但是这些变量在main中被创建,这里它们会持续到该语句完成。
这只是一个代码示例,由于详细的重载解析规则。此外,请确保在函数调用之前声明函数的所有重载版本。这是因为在一个对应函数被调用时,不是所有的重载函数都是可见的,可能会导致问题。比如,在max的三参数版本中,就没有看见max的特殊两ints参数版本,导致三参数版本使用两参数模板:

#include <iostream>
// maximum of two values of any type:
template<typename T>
T max (T a, T b)
{
    std::cout << "max<T>() \n";
    return b < a ? a : b;
}
// maximum of three values of any type:
template<typename T>
T max (T a, T b, T c)
{
return max (max(a,b), c);   // uses the template version even for ints
//because the following declaration comes
// too late:
}
// maximum of two int values:
int max (int a, int b)
{
    std::cout << "max(int,int) \n";
    return b < a ? a : b;
}
int main()
{
    ::max(47,11,33);   // OOPS: uses max<T>() instead of max(int,int)
}

1.6 但是,我们不应该... ?

或许,即使这些简单的函数模板样例可能产生进一步的问题。三个问题可能比较普通,我们这里简单的讨论一下。

1.6.1 传值还是传引用 ?

你可能好奇,为什么一般声明函数通过值传递参数而不是使用引用。一般来说,引用传递是推荐给类型,而不是简单的类型(比如基本类型或者std::string)view),因为并没有创建多余的拷贝。
然而,由于一系列原因,值传递一般来说更合适:

  • 语法简单;
  • 编译器优化更好;
  • 移动语义经常使得拷贝更加便宜;
  • 有时这里根本没有拷贝或者移动;
    此外,对于模板,可以扮演特定角色:
  • 一个模板可能既用于简单类型及复杂类型,所以选择给复杂类型的方式可能对于简单类型会适得其反;
  • 作为调用者,你通常可以决定使用引用传参,使用std::ref()和std::cref();
  • 尽管传递字符常量或者原生数组会成为一个问题,将其通过引用传递可能会导致更大的问题。所有的这一切都在第7章进行讨论。当前我们尽可能考虑用值传递除非只能通过引用传递。

1.6.2 为什么不内联?

一般而言,函数模板并不会用内联进行声明。不像一般函数,我们可以在头文件中定义非内联函数模板,在多个翻译单元中包括该文件。
该规则的唯一例外是特定类型的完全特化,这样结果代码不再是泛型。
从严格的语言定义角度来看,inline就只意味着函数的定义可以多次出现在一个程序中。然而,其也意味着对编译器的提示,对该函数的调用应该是"扩展内联的":这么做可以在某些确定情况下生成更高效的代码。如今,编译器更擅长于在不使用内联关键字的暗示下决定此操作。然而,编译器仍然要考虑在该决定中存在内联。

1.6.3 为什么不constexpr?

自从C++11,你可以使用constexpr来提供使用代码来在编译期计算一些值的能力。对于很多模板而言,这个很重要。
比如,为了在编译期使用max()函数,你不得不如下声明它:

basics/maxconstexpr.hpp
template<typename T1, typename T2>
constexpr auto max (T1 a, T2 b)
{
    return b < a ? a : b;
}

伴随这个,你可以在编译期上下文中使用max()函数模板,比如在声明原生数组大小时:

int a[::max(sizeof(char), 1000u)];

或者std::array<>的大小:

std::array<std::string, ::max(sizeof(char), 1000u)> arr;

注意到我们传递1000作为无符号整形来避免在模板内部出现有符号和无符号比较的的比较警告。
后面会讨论这个,当前只聚焦与基础部分,选择跳过constexpr。