推断的过程
- 对函数调用,推断会比较实参类型和模板参数类型(即T),对要被推断的参数分别推断出替换,每个实参-参数对的分析都是独立的,如果结果矛盾推断就会失败
template<typename T>
T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
auto g = max(1, 1.0); // int和double,推断矛盾
- 即使所有被推断的模板参数不发生矛盾,也可能推断失败
template<typename T>
typename T::ElementT at (T a, int i)
{
return a[i];
}
void f (int* p)
{
int x = at(p, 7);
}
- T被推断成int*,用int*替换返回类型T::ElementT中的T会导致无效的构造,从而推断失败
- 实参-参数对的匹配规则,下面用A表示实参类型,P表示参数化类型。如果参数是一个引用声明(T&),则P就是所引用的类型(T),A仍是实参类型,否则P是所生命的参数类型,A是实参类型。如果实参类型是数组或函数类型,会隐式转为指针类型,并忽略顶层const和volatile限定符
template<typename T> void f(T); // P is T
template<typename T> void g(T&); // P is also T
double x[20];
int const seven = 7;
f(x); // nonreference parameter: T is double*
g(x); // reference parameter: T is double[20]
f(seven); // nonreference parameter: T is int
g(seven); // reference parameter: T is int const
f(7); // nonreference parameter: T is int
g(7); // reference parameter: T is int => ERROR: can't pass 7 to int&
template<typename T>
T const& max(T const& a, T const& b);
- 对
max("Apple", "Pear")
期望T被推断成char const*,但Apple
类型是char const[6],而Pear
是char const[5],推断的是引用参数,不存在数组到指针的转换,因此类型不一致导致推断失败
推断的上下文(Deduced Context)
template<typename T>
void f1(T*);
template<typename E, int N>
void f2(E(&)[N]);
template<typename T1, typename T2, typename T3>
void f3(T1 (T2::*)(T3*));
class S {
public:
void f(double*);
};
void g (int*** ppp)
{
bool b[42];
f1(ppp); // deduces T to be int**
f2(b); // deduces E to be bool and N to be 42
f3(&S::f); // deduces T1 = void, T2=S, and T3 = double
}
- 复杂的类型声明的匹配过程从最顶层构造开始,然后不断递归子构造,即各种组成元素,这些构造被称为推断的上下文。下列构造不能作为推断的上下文用来推断模板参数T
- 受限类型名称,如
Q<T>::X
- 非类型表达式,如
S<I+1>
不能用来推断I
,也不能通过类似int(&)[sizeof(S<T>)]
类型的参数推断T
- 不能推断的上下文不代表程序是错误的
template <int N>
class X {
public:
using I = int;
void f(int) {
}
};
template<int N>
void fppm(void (X<N>::*p)(X<N>::I));
int main()
{
fppm(&X<33>::f); // 正确:N被推断为33
}
- 函数模板
fppm()
中,子构造X<N>::I
是nondeduced context,但具有成员指针类型X<N>::*p
的成员类型部分X<N>
是deducible context,并由此获得参数N,然后把N放入nondeduced contextX<N>::I
,就能获得和实参&X<33>::f
匹配的类型,因此实参-参数对推断成功
- 相反,参数类型完全依赖于deduced contexts也可能导致推断矛盾
template<typename T>
void f(X<Y<T>, Y<T>>);
void g()
{
f(X<Y<int>, Y<int>>()); // OK
f(X<Y<int>, Y<char>>()); // ERROR: deduction fails
}
特殊的推断情况
- 存在两种特殊情况,用于推断的实参-参数对
(A, P)
,实参A不是来自一个函数调用,参数P不是来自函数模板
- 第一种情况出现在取函数模板地址时,此时P是函数模板声明符的参数化类型,而A是被初始化或赋值的的指针代表的函数类型
template<typename T>
void f(T, T);
void (*pf)(char, char) = &f;
- 上例中P就是
void(T, T)
,而A是void(char, char)
,用char替换T推断成功,pf被初始化为特化的f<char>
的地址
- 第二种情况是和转型运算符模板一起出现
class S {
public:
template<typename T, int N> operator T&();
};
void f(int (&)[20]);
void g(S s)
{
f(s);
}
- 这种情况下实参-参数对
(A, P)
涉及到要转型的实参和转型运算符的返回类型,这里S要转型为int (&)[20]
,则A是int[20]
,P是T[N]
,T被int替换,N被20替换,推断成功
初始化列表(Initializer List)
- 函数调用的实参是一个初始化列表时,实参没有具体类型,不会发生推断
#include <initializer_list>
template<typename T> void f(T p);
int main() {
f({1, 2, 3}); // ERROR: cannot deduce T from a braced list
}
- 然而如果参数类型P,在引用和顶层const和volatile限定符移除后,等价于std::initializer_list<P′>,其中P'为有可推断模式的类型,比较P'和初始化列表的每个元素类型,如果所有元素有相同的类型则可以成功推断
#include <initializer_list>
template<typename T>
void f(std::initializer_list<T>);
int main()
{
f({2, 3, 5, 7, 9}); // OK: T is deduced to int
f({'a', 'e', 'i', 'o', 'u', 42}); // ERROR: T deduced to both char and int
}
- 类似地,如果参数类型P是一个数组类型的引用,元素类型P'有可推断模式,比较P'和每个初始化列表中的每个元素类型,若所有元素类型相同则推断成功
参数包
template<typename First, typename... Rest>
void f(First first, Rest... rest);
void g(int i, double j, int* k)
{
f(i, j, k); // deduces First to int, Rest to {double, int*}
}
template<typename T, typename U> class pair { };
template<typename T, typename... Rest>
void h1(pair<T, Rest> const&...);
template<typename... Ts, typename... Rest>
void h2(pair<Ts, Rest> const&...);
void foo(pair<int, float> pif, pair<int, double> pid,
pair<double, double> pdd)
{
h1(pif, pid); // OK: deduces T to int, Rest to {float, double}
h2(pif, pid); // OK: deduces Ts to {int, int}, Rest to {float, double}
h1(pif, pdd); // ERROR: T deduced to int from the 1st arg, but to double from the 2nd
h2(pif, pdd); // OK: deduces Ts to {int, double}, Rest to {float, double}
}
- 参数包推断不仅限于实参-参数对来自调用实参的函数参数包,只要包扩展在函数参数列表或模板实参列表的末尾,就可以进行推断
template<typename... Types> class Tuple { };
template<typename... Types>
bool f1(Tuple<Types...>, Tuple<Types...>);
template<typename... Types1, typename... Types2>
bool f2(Tuple<Types1...>, Tuple<Types2...>);
void bar(Tuple<short, int, long> sv,
Tuple<unsigned short, unsigned, unsigned long> uv)
{
f1(sv, sv); // OK: Types is deduced to {short, int, long}
f2(sv, sv); // OK: Types1 is deduced to {short, int, long},
// Types2 is deduced to {short, int, long}
f1(sv, uv); // ERROR: Types is deduced to {short, int, long} from the 1st arg, but
// to {unsigned short, unsigned, unsigned long} from the 2nd
f2(sv, uv); // OK: Types1 is deduced to {short, int, long},
// Types2 is deduced to {unsigned short, unsigned, unsigned long}
}
字面值运算符模板(Literal Operator Template)
template<char...> int operator "" _B7(); // #1
...
int a = 121_B7; // #2
- 这里2处的初始化包含一个用户自定义的字面值,它会转为字面值运算符模板的调用,模板实参列表为<'1', '2', '3'>,因此如下字面值运算符的实现将对121.5_B7输出'1' '2' '1' '.' '5'
template<char... cs>
int operator"" _B7()
{
std::array<char,sizeof...(cs)> chars{cs...}; // initialize array of passed char
for (char c : chars) { //and use it (print it here)
std::cout << "'" << c << "'";
}
std::cout << '\n';
return ...;
}
- 这个技术只支持即使没有后缀(suffix)也有效的数字字面值
auto b = 01.3_B7; // OK: deduces <'0', '1', '.', '3'>
auto c = 0xFF00_B7; // OK: deduces <'0', 'x', 'F', 'F', '0', '0'>
auto d = 0815_B7; // ERROR: 8 is no valid octal literal
auto e = hello_B7; // ERROR: identifier hello_B7 is not defined
auto f = "hello"_B7; // ERROR: literal operator _B7 does not match
右值引用
- C++11引入了右值引用来实现新技术,包括移动语义和完美转发
引用折叠规则
![](https://img.haomeiwen.com/i5587614/f50245933087cdef.png)
引用折叠规则
using RCI = int const&;
RCI volatile&& r = 42; // OK: r has type int const&
using RRI = int&&;
RRI const&& rr = 42; // OK: rr has type int&&
转发引用(forwarding reference)
- 转发引用是一个函数模板的模板参数的右值引用
- 当函数参数是一个转发引用时,模板实参推断不仅考虑函数调用实参类型,还会考虑实参是左值还是右值
- 实参为左值则参数类型推断为实参类型的左值引用,引用折叠规则将确保替换的参数是一个左值引用
- 实参为右值则参数类型推断为实参类型(不是引用类型),替换的参数是一个此类型的右值引用
template<typename T>
void f(T&& p); // p is a forwarding reference
void g()
{
int i;
int const j = 0;
f(i); // argument is an lvalue; deduces T to int& and
// parameter p has type int&
f(j); // argument is an lvalue; deduces T to int const&
// parameter p has type int const&
f(2); // argument is an rvalue; deduces T to int
// parameter p has type int&&
}
- T为引用类型,推断过程会对模板实例化有一些有趣的影响。实例化一个左值后,一个声明为类型T的局部变量将有引用类型,因此需要初始化
template<typename T>
void f(T&&) // p is a forwarding reference
{
T x; // for passed lvalues, x is a reference
...
}
- 这意味着f()必须知道如何使用类型T,否则函数模板就不能正常支持左值实参,为了解决这个问题,通常会用std::remove_reference来确保x不是一个引用
template<typename T>
void f(T&&) // p is a forwarding reference
{
std::remove_reference_t<T> x; // x is never a reference
...
}
完美转发
- 引用折叠的的规则使得函数模板可以接受几乎任何实参并捕获它的关键属性(类型以及是左值还是右值),由此函数模板能转发实参给另一个函数,这种技术称为完美转发
class C {
...
};
void g(C&);
void g(C const&);
void g(C&&);
template<typename T>
void forwardToG(T&& x)
{
g(static_cast<T&&>(x)); // forward x to g()
}
void foo()
{
C v;
C const c;
forwardToG(v); // eventually calls g(C&)
forwardToG(c); // eventually calls g(C const&)
forwardToG(C()); // eventually calls g(C&&)
forwardToG(std::move(v)); // eventually calls g(C&&)
}
- 在标准库头文件<utility>中提供了函数模板std::forward<>(),用来替代static_cast实现完美转发
#include <utility>
template<typename T>
void forwardToG(T&& x)
{
g(std::forward<T>(x)); // forward x to g()
}
- 完美转发也可以结合可变参数模板,来接收任意数量实参并把每个转发给另一个函数
template<typename... Ts> void forwardToG(Ts&&... xs)
{
g(std::forward<Ts>(xs)...); // forward all xs to g()
}
- 完美转发并不是真正的完美,它不能捕获所有特性,比如不能区分一个左值是否为一个bit-field左值,也不能确定表达式是否有具体的常量值,后者在处理空指针常量时会造成问题,整型值会被当作常量值0
void g(int*);
void g(...);
template<typename T>
void forwardToG(T&& x)
{
g(std::forward<T>(x)); // forward x to g()
}
void foo()
{
g(0); // calls g(int*)
forwardToG(0); // eventually calls g(...)
}
g(nullptr); // calls g(int*)
forwardToG(nullptr); // eventually calls g(int*)
- 完美转发调用另一个函数的返回类型时,可以使用decltype
template<typename... Ts>
auto forwardToG(Ts&&... xs) -> decltype(g(std::forward<Ts>(xs)...))
{
return g(std::forward<Ts>(xs)...); // forward all xs to g()
}
- C++14引入了decltype(auto)简化这个情形
template<typename... Ts>
decltype(auto) forwardToG(Ts&&... xs)
{
return g(std::forward<Ts>(xs)...); // forward all xs to g()
}
Deduction Surprise
- 右值引用的特殊推断规则在完美转发时十分有用,但可能造成出乎意料的结果,因为函数模板通常在函数签名中概括类型,而不影响允许使用实参的类型
void int_lvalues(int&); // accepts lvalues of type int
template<typename T>
void lvalues(T&); // accepts lvalues of any type
void int_rvalues(int&&); // accepts rvalues of type int
template<typename T>
void anything(T&&); // SURPRISE: accepts lvalues and rvalues of any type
template<typename T>
class X
{
public:
X(X&&); // X is not a template parameter
X(T&&); // this constructor is not a function template
template<typename Other>
X(X<U>&&); // X<U> is not a template parameter
template<typename U>
X(U, T&&); // T is a template parameter from an outer template
};
- 当出现问题时,可以用SFINAE结合type trait如std::enable_if来把模板限制于右值
template<typename T>
typename std::enable_if<!std::is_lvalue_reference<T>::value>::type
rvalues(T&&); // accepts rvalues of any type
SFINAE (substitution failure is not an error)
- SFINAE是模板实参推断的一个重要方面,它用来禁止不相关函数模板在重载解析时造成错误
template<typename T, unsigned N>
T* begin(T (&array)[N])
{
return array;
}
template<typename Container>
typename Container::iterator begin(Container& c)
{
return c.begin();
}
int main()
{
std::vector<int> v;
int a[10];
::begin(v); // OK: only container begin() matches, because the first deduction fails
::begin(a); // OK: only array begin() matches, because the second substitution fails
}
即时上下文(Immediate Context)
- SFINAE保护防止形成无效类型或表达式的尝试,包括发生在函数模板替换的immediate context中的二义性或非法访问控制错误
- 具体地,在函数模板替换时,任何发生于以下实例化期间的事,以及由替换过程触发的特殊成员函数的任何隐式定义,都不属于immediate context的部分,其他所有事则都属于immediate context
- 类模板定义
- 函数模板定义
- 变量模板初始化
- 默认实参
- 默认成员初始化
- 异常执行顺序(exception specification)
- 因此如果替换一个函数模板声明的模板参数,需要类模板主体的实例化,因为类的一个成员正被引用,在此实例化期间的错误不在函数模板替换的immediate context中,因此是一个真正的错误(即使另一个函数模板匹配无误),SFINAE不会被使用
template<typename T>
class Array {
public:
using iterator = T*;
};
template<typename T>
void f(Array<T>::iterator first, Array<T>::iterator last);
template<typename T>
void f(T*, T*);
int main()
{
f<int&>(0, 0); // ERROR: substituting int& for T in the first function template
} // instantiates Array<int&>, which then fails
- 下面是一个C++14的例子,依赖于推断的返回类型,在函数模板定义实例化期间产生一个错误
template<typename T> auto f(T p) {
return p->m;
}
int f(...);
template<typename T>
auto g(T p) -> decltype(f(p));
int main()
{
g(42);
}
- g(42)把T推断为int,于是有两个可以使用的f()匹配,非模板函数能匹配,但因为省略号参数不是特别好的匹配。而模板有一个推断的返回类型,所以必须实例化定义来确定返回类型,当p为int时p->m是无效的,错误发生在替换的immediate context之外,于是产生错误,因此如果返回类型能轻易地显式指定,最好禁用推断的返回类型
- SFINAE最初的目的是消除意外的函数模板重载匹配的错误,就像begin()的例子,然而检测一个无效表达式或类型的能力实现了强大的编译期技术,允许确定一个特定语法是否有效
推断的限制
可接受的实参转换
- 当找不到使P类型等同于A类型的精确匹配时,下面几种变化是可接受的
- 如果原来的参数是一个reference declarator,被替换的P类型可以比A多一个const或volatile限定符
- A类型是指针或成员指针类型,可以进行限定符转换(添加const或volatile),再转化为被替换的P类型
- 推断过程不涉及转型运算符模板时,被替换的P类型可以是A类型的基类,或者P是指向A类型(派生类)的基类的指针,参考下列代码
template<typename T>
class B<T> {
};
template<typename T>
class D : B<T> {
};
template<typename T>
void f(B<T>*);
void g(D<long> dl)
{
f(&dl); // 推断成功:用long替换T
}
- 如果P在deduced context中不包含一个模板参数,所有隐式转寒都是允许的
template<typename T> int f(T, typename T::X);
struct V {
V();
struct X {
X(double);
};
} v;
int r = f(v, 7.0); // OK: T is deduced to int through the first parameter,
// which causes the second parameter to have type V::X
// which can be constructed from a double value
- 忽略可用于函数实参以使调用成功的各种转换,这些规则范围十分狭窄
std::string maxWithHello(std::string s)
{
return ::max(s, "hello"); // first argument deduces T to std::string
// second argument deduces T to char[6]
// so template argument deduction fails
}
类模板实参
- 模板实参推断只能用于函数模板和成员函数模板,而不能用于类模板,不能从类模板的构造函数的实参推断类模板参数
template<typename T>
class S {
public:
S(T b) : a(b) {}
private:
T a;
};
S x(12); // 错误:不能从构造函数实参推断类模板参数T
默认调用实参
- 函数模板中可以指定默认实参,默认实参可以依赖于模板参数,没有提供显式实参时才会实例化默认实参
template<typename T>
void init (T* loc, T const& val = T())
{
*loc = val;
}
class S {
public:
S(int, int);
};
S s(0, 0);
int main()
{
init(&s, S(7, 42)); // 因为T=S, 所以默认实参T()无需实例化
}
- 即使默认调用实参是非依赖的, 也不能用于推断模板实参
template<typename T>
void f (T x = 42)
{
}
int main()
{
f<int>(); // OK: T = int
f(); // ERROR: cannot deduce T from default call argument
}
Exception Specification
- exception specification类似于默认调用实参,只在需要时初始化,它表示它们不参与模板实参推断
template<typename T>
void f(T, int) noexcept(nonexistent(T()));// #1
template<typename T>
void f(T, ...); // #2 (C-style vararg function)
void test(int i)
{
f(i, i); // ERROR: chooses #1 , but the expression nonexistent(T()) is ill-formed
}
template<typename T>
void g(T, int) throw(typename T::Nonexistent); // #1
template<typename T>
void g(T, ...); // #2
void test(int i)
{
g(i, i);// ERROR: chooses #1 , but the type T::Nonexistent is ill-formed
}
- 这些动态的exception specification在C++11中被抛弃,且在C++17中被移除
显式函数模板实参
- 当函数模板实参不能被推断时,可以在函数模板名后显式指定它
template<typename T>
T default_value()
{
return T{};
}
int main()
{
return default_value<int>();
}
- 一旦模板实参被显式指定,对应的实参就不会被推断,所以允许隐式转换,实参2被隐式转换为double
- 显式指定总是由左到右匹配模板参数,所以不能被推断的实参应该首先指定
template<typename Out, typename In>
Out convert(In p)
{
...
}
int main() {
auto x = convert<double>(42); // the type of parameter p is deduced,
// but the return type is explicitly specified
}
- 当仍然使用推断来确定模板实参时,指定空模板实参列表可以确保选中函数时一个模板实例
int f(int); // #1
template<typename T> T f(T); // #2
int main() {
auto x = f(42); // calls #1
auto y = f<>(42); // calls #2
}
- 在友元函数声明的语境中,显式模板实参列表的存在有一个有趣的影响
void f();
template<typename> void f();
namespace N {
class C {
friend int f(); // OK
friend int f<>(); // ERROR: return type conflict
};
}
- 显式指定模板实参替换会使用SFINAE原则:如果替换导致一个immediate context中的错误,函数模板就会被丢弃,但其他模板仍能成功使用
template<typename T> typename T::EType f();// #1
template<typename T> T f();// #2
int main() {
auto x = f<int*>(); // select #2,x is a pointer to function
}
template<typename T> void f(T);
template<typename T> void f(T, T);
int main() {
auto x = f<int*>; // ERROR: there are two possible f<int*> here
}
template<typename ... Ts> void f(Ts ... ps);
int main() {
f<double, double, int>(1, 2, 3); // OK: 1 and 2 are converted to double
}
template<typename ... Ts> void f(Ts ... ps);
int main() {
f<double, int>(1, 2, 3); // OK: the template arguments are <double, int, int>
}
从初始化和表达式推断
auto类型说明符
template<typename Container>
void useContainer(Container const& container)
{
auto pos = container.begin();
while (pos != container.end()) {
auto& element = *pos++;
... // operate on the element
}
}
- auto的推断使用模板实参推断相同的机制。auto被一个虚构的模板类型参数T替代,然后进行推断,推断过程相当于变量是一个函数参数,其初始化相当于对应的函数实参。上例中第一个auto等价下列情形,T就是为auto推断的类型
// auto pos = container.begin()等价于
template<typename T> void deducePos(T pos);
deducePos(container.begin());
- auto类型变量不会是引用类型,所以要用auto&,第二个auto推断等价于
// auto& element = *pos++等价于
template<typename T> void deduceElement(T& element);
deduceElement(*pos++);
- auto也可以结合右值引用,但是这样会使其行为像一个转发引用
auto&& fr = ...;
// 等价于
template<typename T> void f(T&& fr); // auto被T替换
int x;
auto&& rr = 42; // OK: 右值引用绑到一个右值(auto = int)
auto&& lr = x; // OK: auto = int&,引用折叠使lr为左值引用
- 这个技术通常用于泛型编程来绑定一个值类型未知(左值或右值)的函数或操作符调用,如范围for循环
template<typename Container> void g(Container c) {
for (auto&& x: c) {
...
}
}
- auto可以绑定到const,但auto必须是主类型修饰符
template<typename T> struct X { T const m; };
auto const N = 400u; // OK: constant of type unsigned int
auto* gp = (void*)nullptr; // OK: gp has type void*
auto const S::*pm = &X<int>::m; // OK: pm has type int const X<int>::*
X<auto> xa = X<int>(); // ERROR: auto in template argument
int const auto::*pm2 = &X<int>::m; // ERROR: auto is part of the “declarator”
auto f() { return 42; }
// 也能用尾置返回类型
auto f() -> auto { return 42; }
// 第一个auto声明尾置返回类型
// 第二个auto是用于推断的占位符类型
- lambda默认存在相同的机制:如果返回类型没有显式声明,则返回类型被视为auto
auto lm = [] (int x) { return f(x); };
// same as: [] (int x) -> auto { return f(x); };
- 返回类型推断的函数也能分开声明和定义,不过前置声明作用十分有限,因为定义必须在任何使用函数的地方可见,提供一个确定返回类型的前置声明是非法的
auto f(); // forward declaration
auto f() { return 42; }
int known();
auto known() { return 42; } // ERROR: incompatible return type
- 这种前置声明一般用于移动一个类定义外的成员函数定义
struct S {
auto f(); // the definition will follow the class definition
};
auto S::f() { return 42; }
- C++17之前,非类型模板实参必须用具体类型声明,它可以是模板参数类型
template<typename T, T V> struct S;
S<int, 42>* ps;
- 这里必须确定实参类型int,过于冗长繁琐,C++17允许由模板实参推断实际类型
template<auto V> struct S;
S<42>* ps; // V is deduced to be int
S<42u> us; // V is deduced to be unsigned int
S<3.14>* pd;// ERROR: floating-point nontype argument
- 如果在定义中需要表示对应实参的类型可以用decltype
template<auto V> struct Value {
using ArgType = decltype(V);
};
template<typename> struct PMClassT;
template<typename C, typename M>
struct PMClassT<M C::*> {
using Type = C;
};
template<typename PM>
using PMClass = typename PMClassT<PM>::Type;
template<auto PMD>
struct CounterHandle {
PMClass<decltype(PMD)>& c;
CounterHandle(PMClass<decltype(PMD)>& c): c(c) {
}
void incr() {
++(c.*PMD);
}
};
struct S {
int i;
};
int main() {
S s{41};
CounterHandle<&S::i> h(s);
h.incr(); // increases s.i
}
- 这里使用一个辅助类模板PMClassT来从一个指向成员的指针类型获取父类类型,使用auto模板参数只要指定指向成员的指针常量&S::i作为模板实参,而C++17前必须指定冗长的具体类型
OldCounterHandle<int S::*, &S::i>
template<auto... VS>
struct Values {
};
Values<1, 2, 3> beginning;
Values<1, 'x', nullptr> triplet;
template<auto V1, decltype(V1)... VRest> struct HomogeneousValues {
};
用decltype表示表达式类型
- decltype允许表达一个表达式或声明的精确类型,但要注意传递的实参是声明实例还是表达式
- 如果e是一个实例名称(如变量、函数、枚举、数据成员)或一个类成员access,decltype(e)产生实例或类成员实例的声明类型。当想匹配已有声明类型时
auto x = ...;
auto y1 = x + 1;
decltype(x) y2 = x + 1;
- y1的类型可能与x相同也可能不同,取决于x的初始化和+,如果x被推断为int,y1也会是int,如果x是char,y1会是int(因为char+1和为int),而decltype(x)保证y2总是和x类型相同
- 反之,如果e是其他表达式,decltype(e)产生一个反映表达式type或value的类型
- e是T类型lvalue,产生T&
- e是T类型xvalue,产生T&&
- e是T类型prvalue,产生T
void g (std::string&& s)
{
// check the type of s:
std::is_lvalue_reference<decltype(s)>::value; // false
std::is_rvalue_reference<decltype(s)>::value; // true (s as declared)
std::is_same<decltype(s),std::string&>::value; // false
std::is_same<decltype(s),std::string&&>::value; // true
// check the value category of s used as expression:
std::is_lvalue_reference<decltype((s))>::value; // true (s is an lvalue)
std::is_rvalue_reference<decltype((s))>::value; // false
std::is_same<decltype((s)),std::string&>::value; // true (T& signals an lvalue)
std::is_same<decltype((s)),std::string&&>::value; // false
}
- decltype在auto无法产生足够的值推断时能派上用场
// 总会产生一个元素的拷贝
auto element = *pos;
// 如果替换如下则总接收一个元素的引用
// 如果operator*返回一个值将出错
auto& element = *pos;
// 使用decltype来使值和引用都能保存
decltype(*pos) element = *pos;
decltype(auto)
int i = 42; // i has type int
int const& ref = i; // ref has type int const& and refers to i
auto x = ref; // x1 has type int and is a new independent object
decltype(auto) y = ref; // y has type int const& and also refers to i
std::vector<int> v = { 42 };
auto x = v[0]; // x denotes a new object of type int
decltype(auto) y = v[0]; // y is a reference (type int&)
decltype(*pos) element = *pos;
// 等价于
decltype(auto) element = *pos;
template<typename C> class Adapt
{
C container;
...
decltype(auto) operator[] (std::size_t idx) {
return container[idx];
}
};
- 不同于auto,decltype(auto)不允许限定符或声明符修饰类型
decltype(auto)* p = (void*)nullptr; // invalid
int const N = 100;
decltype(auto) const NN = N*N; // invalid
int x;
decltype(auto) z = x; // object of type int
decltype(auto) r = (x); // reference of type int&
int g();
...
decltype(auto) f() {
int r = g();
return (r); // run-time ERROR: returns reference to temporary
}
- C++17开始decltype(auto)也可以用于推断非类型参数
template<decltype(auto) Val> class S
{
...
};
constexpr int c = 42;
extern int v = 42;
S<c> sc; // #1 produces S<42>
S<(v)> sv; // #2 produces S<(int&)v>
template<auto N> struct S {};
template<auto N> int f(S<N> p);
S<42> x;
int r = f(x);
template<auto V> int f(decltype(V) p);
int r1 = deduce<42>(42); // OK
int r2 = deduce(42); // ERROR: decltype(V) is a nondeduced context
auto的特殊情况
- 当变量初始值是一个初始化列表,对应的函数调用推断将失败
template<typename T>
void deduceT (T);
...
deduceT({ 2, 3, 4}); // ERROR
deduceT({ 1 }); // ERROR
template<typename T>
void deduceInitList(std::initializer_list<T>);
...
deduceInitList({ 2, 3, 5, 7 }); // OK: T deduced as int
// 等价于
auto primes = { 2, 3, 5, 7 };
deduceT(primes); // T deduced as std::initializer_list<int>
- C++17之前的直接初始化(不用=)也是这样处理的,但C++17中被改动以更好地匹配期望的行为
auto oops { 0, 8, 15 }; // ERROR in C++17
auto val { 2 }; // OK: val has type int in C++17
- C++17前上例中的初始化都是有效的,oops和val都会被初始化为类型initializer_list<int>
- 返回一个初始化列表给返回类型为可推断占位符类型函数是无效的
auto subtleError() {
return { 1, 2, 3 }; // ERROR
}
char c;
auto *cp = &c, d = c; // OK
auto e = c, f = c+1; // ERROR: deduction mismatch char vs. int
auto f(bool b) {
if (b) {
return 42.0; // deduces return type double
} else {
return 0; // ERROR: deduction conflict
}
}
- 如果返回的表达式递归调用函数,不会发生推断,将出错,除非已有一个确定返回类型的推断
// 错误例子
auto f(int n)
{
if (n > 1) {
return n*f(n-1); // ERROR: type of f(n-1) unknown
} else {
return 1;
}
}
// 等价的正确例子
auto f(int n)
{
if (n <= 1) {
return 1; // return type is deduced to be int
} else {
return n*f(n-1); // OK: type of f(n-1) is int and so is type of n*f(n-1)
}
}
- 可推断的返回类型另一个特殊情况是,在可推断的变量类型或可推断的非类型参数类型中没有对应的副本,返回类型会推断为void,若不能匹配void则出错
auto f1() { } // OK: return type is void
auto f2() { return; } // OK: return type is void
auto* f3() {} // ERROR: auto* cannot deduce as void
template<typename T, typename U>
auto addA(T t, U u) -> decltype(t+u)
{
return t + u;
}
void addA(...);
template<typename T, typename U>
auto addB(T t, U u) -> decltype(auto)
{
return t + u;
}
void addB(...);
struct X {
};
using AddResultA = decltype(addA(X(), X()));
// OK: AddResultA is void
using AddResultB = decltype(addB(X(), X()));
// ERROR: instantiation of addB<X> is ill-formed
- addB()用decltype(auto)而不是decltype(t+u)会在重载解析时造成错误,addB()的函数体必须被完整实例化来确定返回类型,而这个实例化不是immediate context,SFINAE不适用。因此可推断的返回类型不只是复杂显式返回类型的缩写,在其他不会使用SFINAE的函数模板签名中,decltype(auto)不应该被调用
结构化绑定(Structured Binding)
- 结构化绑定是C++17引入的新特性,作用是在一次声明中引入多个变量
struct MaybeInt { bool valid; int value; };
MaybeInt g();
auto const&& [b, N] = g(); // binds b and N to the members of the result of g()
- 一个结构化绑定必须总有一个auto,可以用cv限定符或&、&&声明符(但不能是*指针声明符)
- 有三种不同类型的实例可以初始化一个结构化绑定,分别是类类型,数组,std::tuple-like的类(通过get<>绑定)
- 数组的例子
int main() {
double pt[3];
auto& [x, y, z] = pt;
x = 3.0; y = 4.0; z = 0.0;
plot(pt);
}
auto f() -> int(&)[2]; // f() returns reference to int array
auto [ x, y ] = f(); // #1
auto& [ r, s ] = f(); // #2
- 1处的实例e会按如下推断,但不会推断出指向数组的指针,e被推断为一个对应于初始化类型的数组类型的变量,接着拷贝数组的每个元素,因此x和y就分别变成了e[0]和e[1]的别名
auto e = f();
- 2处不会调用数组拷贝,而是按auto通用的规则产生一个数组的引用,x和y同样也变成了e[0]和e[1]的别名
auto& e = f();
- 对于std::tuple-like的类,E为表达式(e)的类型,因为E为一个表达式的类型,所以它永远不会是一个引用类型,如果表达式std::tuple_size<E>::value是一个有效的整型常量表达式,它必须等于中括号标识符的数量,用n0、n1、n2表示中括号标识符,如果e有名为get的成员,表现就如同这些标识符声明如下
std::tuple_element<i, E>::type& ni = e.get<i>();
std::tuple_element<i, E>::type&& ni = e.get<i>();
std::tuple_element<i, E>::type& ni = get<i>(e);
std::tuple_element<i, E>::type&& ni = get<i>(e);
- get只是用来查找关联类和命名空间,std::tuple, std::pair和 std::array模板都实现了这个协议
#include <tuple>
std::tuple<bool, int> bi {true, 42};
auto [b, i] = bi;
int r = i; // initializes r to 42
- 添加std::tuple_size和std::tuple_element的特化不难,函数模板或成员函数模板get<>()将使这个机制可以用于任何类或枚举,只需要包含<utility>来使用这两个tuple-like access辅助函数std::tuple_size<>和std::tuple_element<>
#include <utility>
enum M {};
template<> class std::tuple_size<M> {
public:
static unsigned const value = 2; // map M to a pair of values
};
template<> class std::tuple_element<0, M> {
public:
using type = int; // the first value will have type int
};
template<> class std::tuple_element<1, M> {
public:
using type = double; // the second value will have type double
};
template<int> auto get(M);
template<> auto get<0>(M) { return 42; }
template<> auto get<1>(M) { return 7.0; }
auto [i, d] = M(); // as if: int&& i = 42; double&& d = 7.0;
泛型lambada(Generic Lambda)
template<typename Iter>
Iter findNegative(Iter first, Iter last)
{
return std::find_if(first, last,
[] (typename std::iterator_traits<Iter>::value_type value) {
return value < 0;
});
}
- C++14引用了泛型lambda,参数类型可以为auto
template<typename Iter>
Iter findNegative(Iter first, Iter last)
{
return std::find_if(first, last,
[] (auto value) {
return value < 0;
});
}
- lambda创建时不知道实参类型,推断不会立即进行,而是先把模板类型参数添加到模板参数列表中,这样lambda就可以被任何实参类型调用,只要实参类型支持< 0操作,结果能转为bool
[] (int i) {
return i < 0;
}
- 编译器把这个表达式编译成一个新创建类的实例,这个实例称为闭包(closure)或闭包对象(closure object),这个类称为闭包类型(closure type)。闭包类型有一个函数调用运算符(function call operator),因此闭包是一个函数对象。上面这个lambda的闭包类型就是一个编译器内部的类,如果检查一个lamdba的类型,std::is_class<>将生成true
class SomeCompilerSpecificNameX
{
public:
SomeCompilerSpecificNameX(); // only callable by the compiler
bool operator() (int i) const
{
return i < 0;
}
};
- 因此一个lambda表达式将产生一个类的对象,下例将创建一个编译器内部类的对象(闭包)
foo(...,
[] (int i) {
return i < 0;
});
// 创建编译器内部类SomeCompilerSpecificNameX
foo(...,
SomeCompilerSpecificNameX{}); // 传递一个闭包类型对象
int x, y;
...
[x,y](int i) {
return i > x && i < y;
}
class SomeCompilerSpecificNameY {
private:
int _x, _y;
public:
SomeCompilerSpecificNameY(int x, int y) //only callable by the compiler
: _x(x), _y(y) {
}
bool operator() (int i) const {
return i > _x && i < _y;
}
};
- 对一个泛型lambda,函数调用操作符将变成一个成员函数模板,因此
[] (auto i) {
return i < 0;
}
class SomeCompilerSpecificNameZ
{
public:
SomeCompilerSpecificNameZ(); // only callable by compiler
template<typename T>
auto operator() (T i) const
{
return i < 0;
}
};
- 当闭包被调用时才会实例化成员函数模板,而不是出现lambda的位置
#include <iostream>
template<typename F, typename... Ts>
void invoke (F f, Ts... ps)
{
f(ps...);
}
int main()
{
invoke([](auto x, auto y) {
std::cout << x+y << '\n'
},
21, 21);
}
- 闭包的调用运算符在主函数中lambda出现的位置未被实例化,在闭包类型作为第一个参数类型,int作为第二个和第三个参数类型时,invoke()函数模板才被实例化,invoke的实例化称为闭包的拷贝,它将实例化闭包的operator()模板以满足实例化调用f(ps...)
别名模板
- 无论带有模板实参的别名模板出现在何处,别名的定义都会被实参替代,产生的模式将用于推断
template<typename T, typename Cont>
class Stack;
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;
template<typename T, typename Cont>
void f1(Stack<T, Cont>);
template<typename T>
void f2(DequeStack<T>);
template<typename T>
void f3(Stack<T, std::deque<T>); // equivalent to f2
void test(DequeStack<int> intStack)
{
f1(intStack); // OK: T deduced to int, Cont deduced to std::deque<int>
f2(intStack); // OK: T deduced to int
f3(intStack); // OK: T deduced to int
}
template<typename T> using A = T;
template<> using A<int> = void; // ERROR, but suppose it were possible
- 假设可以特化,就不能让A<T>匹配类型void,并因为A<int>和A<void>都等于void得出结论T必须为void,事实上保证每个别名模板能通过定义进行泛型扩展是不可能的,这允许它对推断是透明的
类模板实参推断
- C++17允许从变量声明初始化指定的实参,或函数几号类型的转换,推断class type的模板参数
template<typename T1, typename T2, typename T3 = T2>
class C
{
public:
// constructor for 0, 1, 2, or 3 arguments:
C (T1 x = T1{}, T2 y = T2{}, T3 z = T3{});
...
};
C c1(22, 44.3, "hi"); // OK in C++17: T1 is int, T2 is double, T3 is char const*
C c2(22, 44.3); // OK in C++17: T1 is int, T2 and T3 are double
C c3("hi", "guy"); // OK in C++17: T1, T2, and T3 are char const*
C c4; // ERROR: T1 and T2 are undefined
C c5("hi"); // ERROR: T2 is undefined
- 注意所有参数必须通过推断或由默认实参确定,不能显式指定一部分而推断另一部分
C<string> c10("hi","my", 42); // ERROR: only T1 explicitly specified, T2 not deduced
C<> c11(22, 44.3, 42); // ERROR: neither T1 nor T2 explicitly specified
C<string,string> c12("hi","my"); // OK: T1 and T2 are deduced, T3 has default
推断指南(Deduction Guide)
template<typename T>
class S {
private:
T a;
public:
S(T b) : a(b) {
}
};
template<typename T> S(T) -> S<T>; // deduction guide
S x{12}; // OK since C++17, same as: S<int> x{12};
S y(12); // OK since C++17, same as: S<int> y(12);
auto z = S{12}; // OK since C++17, same as: auto z = S<int>{12};
- deduction guide有点像函数模板,但语法上有一些区别
- 看起来像尾置返回类型的部分不能写成传统的返回类型,这个类型(即上例的S<T>)就是guided type
- 尾置返回类型前没有auto关键字
- deduction guide的名称必须是之前在同一作用域声明的类模板的非受限名称
- guide的guided type必须是一个template-id,其template对应guide name
- 能被explicit限定符声明
- 在S x{12}中的限定符S称为一个占位符类类型,使用这样的占位符,必须紧跟被声明的变量名称和一个初始化
S* p = &x;// ERROR: syntax not permitted
- S x(12)通过处理S相关的deduction guide推断变量类型,这里只有一个guide,于是成功将T推断为int,guide的guided type为S<int>,guided type因此被选为声明的类型
- 注意一个类模板名后接多个声明,每个声明的初始化必须产生相同类型,类似于auto
S s1(1), s2(2.0);
// ERROR: deduces S both as S<int> and S<double>
- 之前的例子中,decuction guide和类S声明的构造函数S(T b)有一个隐式关联,但这个关联不是必需的,deduction guide也能用于聚合类模板
template<typename T>
struct A
{
T val;
};
template<typename T> A(T) -> A<T>; // deduction guide
- 如果没有deduction guide就总是要显式指定模板实参
A<int> a1{42}; // OK
A<int> a2(42); // ERROR: not aggregate initialization
A<int> a3 = {42}; // OK
A a4 = 42; // ERROR: can't deduce type
// with the guide as written above, we can write
A a4 = { 42 }; // OK
// initializer must still be a valid aggregate initializer
// it must use a braced initializer list
A a5(42); // ERROR: not aggregate initialization
A a6 = 42; // ERROR: not aggregate initialization
隐式推断指南(Implicit Deduction Guide)
- deduction guide对类模板中的每个构造函数都是值得用的,这使类模板实参推断的设计者引入了一个隐式机制,它等价于为基本类模板的每个构造函数和构函数模板引入一个如下的implicit deduction guide
- implicit guide模板参数列表由类模板的模板参数组成,在构造函数模板的情况下,由构造函数模板的模板参数组成。构造函数模板的模板参数保留任何默认实参
- guide的function-like参数从构造函数或构造函数模板拷贝
- guide的guide type是模板名称,模板的实参是从类模板获得的模板参数
template<typename T>
class S {
private:
T a;
public:
S(T b) : a(b) {
}
};
- 模板参数列表是typename T,function-like参数列表变成(T b),guided type是S<T>,因此获取一个guide,它等价于之前所写的用户声明的guide:因此guide不需要达到我们期望的效果。也就是说只用没有deduction guide的简单类模板,就可以有效地写下S x(12),预期结果是x具有类型S<int>
- deduction guide有一个歧义
S x{12}; // x has type S<int>
S y{s1};
S z(s1);
- 已经知道x有类型S<int>,但y和z的类型,马上会想到的是S<S<int>>和S<int>,而标准委员会有争议地决定两个都是S<int>类型
std::vector v{1, 2, 3}; // vector<int>, not surprising
std::vector w2{v, v}; // vector<vector<int>>
std::vector w1{v}; // vector<int>!
- 换句话说,一个元素初始化的推断和多个元素不同,通常一个元素就是期望的结果,但有一些微妙的区别,泛型编程中,很容易错过这种微妙之处
template<typename T, typename... Ts>
auto f(T p, Ts... ps) {
std::vector v{p, ps...}; // type depends on pack length
...
}
- 这里很容易忘记T被推断为一个vector类型,v的类型将根据ps是空或非空包而产生根本的不同
- 添加implicit template guide是有争议的,主要反对观点是这个特性自动将接口添加到已存在的库中,考虑上面的类模板S,它的定义是有效的,然而,S的作者扩展库将造成S以一个更复杂的方式定义
template<typename T>
struct ValueArg {
using Type = T;
};
template< typename T>
class S {
private:
T a;
public:
using ArgType = typename ValueArg<T>::Type;
S(ArgType b) : a(b) {
}
};
- C++17之前,这样的转变不会影响现有代码,但C++17中它们会禁用implicit deduction guide,为了明白这点,写一个对应于上面的implicit deduction guide生成的模板的deduction guide:模板参数列表和guided type不变,但function-like参数现在根据ArgType写出,为typename ValueArg<T>::Type
template<typename>
S(typename ValueArg<T>::Type) -> S<T>;
- 限定的名称如ValueArg<T>::不是一个可推断的context,所以deduction guide无效,不会产生一个类似S x(12)的声明,换句话说,一个库作者在C++17中执行这种转换可能破坏客户端代码,建议的做法是谨慎考虑每个构造函数是否要提供为一个implicit deduction guide的来源,如果不是则通过类似typename ValueArg<X>::Type,用一个类型X的可推断的构造函数参数替换每个实例
其他细节
插入式类名称
template<typename T> struct X {
template<typename Iter> X(Iter b, Iter e);
template<typename Iter> auto f(Iter b, Iter e) {
return X(b, e); // What is this?
}
};
- 上例在C++14中有效:X(b, e)中的X是一个插入式类型,等价于X<T>,然而类模板实参推断规则将使X等价于X<Iter>
- 为了保持向后兼容性,如果模板名称是插入式类名,则禁用类模板实参推断
转发引用(forwarding reference)
template<typename T> struct Y {
Y(T const&);
Y(T&&);
};
void g(std::string s) {
Y y = s;
}
- 这里的意图明显是通过拷贝构造函数关联的implicit deduction guide,推断T为std::string,然而写下implicit deduction guide作为显式声明guide将造成意外
template<typename T> Y(T const&) -> Y<T>; // #1
template<typename T> Y(T&&) -> Y<T>; // #2
- 对于T&&的行为,作为转发引用,如果是左值,T被推断为一个引用类型。在上例中,实参表达式s是左值。implicit guide1推断T是std::string,但要求实参由std::string转为std::string const,guide2将正常推断T为std::string&并产生一个同类型的参数,是一个更好的匹配。这个结果是十分出乎意料的,且可能导致实例化错误(当类模板参数在不允许引用类型的context中使用),或者更糟的是静默地产生一个无用的实例化(如产生悬挂引用)
- 标准委员会因此决定,当执行implicit deduction guide的推断时,如果T原本是一个类模板参数(而非构造函数模板参数,对那些将保留推断规则),禁用T&&这个特殊的推断规则,因此上例中如预期推断T为std::string
The explicit Keyword
- deduction guide可以用关键词explicit声明,通常用于直接初始化情况而不用于拷贝初始化
template<typename T, typename U> struct Z {
Z(T const&);
Z(T&&);
};
template<typename T> Z(T const&) -> Z<T, T&>; // #1
template<typename T> explicit Z(T&&) -> Z<T, T>; // #2
Z z1 = 1; // only considers #1 ; same as: Z<int, int&> z1 = 1;
Z z2{2}; // prefers #2 ; same as: Z<int, int> z2{2};
Copy Construction and Initializer Lists
template<typename ... Ts> struct Tuple {
Tuple(Ts...);
Tuple(Tuple<Ts...> const&);
};
- 为了理解implicit guide的影响,将它们写为显式声明
template<typename... Ts> Tuple(Ts...) -> Tuple<Ts...>;
template<typename... Ts> Tuple(Tuple<Ts...> const&) -> Tuple<Ts...>;
- 对于下例,将明显选用第一个guide和构造函数,x因此是一个Tuple<int, int>
auto x = Tuple{1,2};
Tuple a = x;
Tuple b(x);
- a和b都匹配两个guide,第一个guide选用类型
Tuple<Tuple<int, int>
,但关联的构造函数生成Tuple<int, int>
,而第二个guide是一个更好的匹配,因此a和b都由x拷贝构造
- 再考虑使用初始化列表的例子
Tuple c{x, x};
Tuple d{x};
- 第一个只能匹配第一个guide,生成
Tuple<Tuple<int, int>, Tuple<int, int>>
,这完全是直觉而非以外,这意味着第二个例子将推断d为类型Tuple<Tuple<int>>
,然而它将被视为一个拷贝构造(即优先选择第二个implicit guide),这也发生于函数符号转换
auto e = Tuple{x};
- e被推断为
Tuple<int, int>
而非<Tuple<int>>
Guides Are for Deduction Only
- deduction guide不是函数模板,它们只用于推断模板参数而非调用,这表示实参的传值或传引用对guide声明不重要
template<typename T> struct X {
...
};
template<typename T> struct Y {
Y(X<T> const&);
Y(X<T>&&);
};
template<typename T> Y(X<T>) -> Y<T>;
- 注意,deduction guide和Y的两个构造函数不对应,然而这不重要,因为guide只用于推断。给定一个类型X<TT>(左值或右值)的值xtt将选用可推断的类型Y<TT>,接着初始化对Y<TT>的构造函数进行重载解析来决定调用哪个(取决于xtt是左值还是右值)
网友评论