wizlyk

wizlyk的代码小天地

0%

Deducing Types - 《Effective Modern C++》阅读笔记(一)

Item 1: 理解模板类型推断
Item 2: auto语句中的类型推断
Item 3: 理解decltype
Item 4: 如何观测实际的类型推断

本文所有内容参考于 《Effective Modern C++》(Scott Meyers)一书,仅供个人学习

Deducing Types (类型推断)

Item 1: 理解模板类型推断

模板函数声明中的类型

1
2
3
4
5
6
template<typename T>
void f(ParamType param);

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

ParamType为普通引用,即 ParamType 为 T&

1
2
template<typename T>
void f(T& param);
1
2
3
4
5
6
void f(T& param);
f(x); // T is int, param's type is int&
f(cx); // T is const int,
// param's type is const int&
f(rx); // T is const int,
// param's type is const int&

ParamType 为 const T&

1
2
3
4
void f(const T& param);
f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&

ParamType 为右值引用 T&&,

1
2
3
4
5
6
7
8
9
void f(T&& param);
f(x); // x is lvalue, so T is int&,
// param's type is also int&
f(cx); // cx is lvalue, so T is const int&,
// param's type is also const int&
f(rx); // rx is lvalue, so T is const int&,
// param's type is also const int&
f(27); // 27 is rvalue, so T is int,
// param's type is therefore int&&

在《C++ Primer》16.2.5中介绍了这种情况:“当我们将一个左值传递给函数的优质引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。”因此f(x)中T为int&, f(cx)中T为const int&。同时,“我们不能(直接)定义一个引用的引用”,这时有另外的一个规则,叫引用折叠

1
2
3
4
引用折叠:
- X& &、X& &&和X&& &都折叠成类型X&
- 类型X&& &&折叠成X&&
- 当传入的是左值时,类型推断会推向左值引用;若传入的是右值才推为右值引用。

ParamType 是值,T

1
2
3
4
void f(T param);
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int

会将传入的值去引用,并去const。

当传入数组会怎么样? 是的,聪明的你会发现下面的格式是错的

1
void myFunc(int param[]);

在数组名字单独出现时,往往被视作一个指针

1
void myFunc(int* param);

然而我们可以声明一个参数是对一个数组的引用。设name是一个长度为13的const char[]。

1
2
3
4
template<typename T>
void f(T& param); // template with by-reference parameter

f(name); // pass array to f

T会被推断为const char [13],f的参数是const char (&)[13]。
进一步的,如果想知道一个数组的大小,可以像下面这样

1
2
3
4
5
template<typename T, std::size_t N> 
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
} // 注意以后会提到的constexpr & noexcept

最后给出书中的tips。

Things to Remember

  • During template type deduction, arguments that are references are treated as non-references, i.e., their reference-ness is ignored.
  • When deducing types for universal reference parameters, lvalue arguments get special treatment.
  • When deducing types for by-value parameters, const and/or volatile arguments are treated as non-const and non-volatile.
  • During template type deduction, arguments that are array or function names decay to pointers, unless they’re used to initialize references.

存在的疑问: 1,什么是Universal reference? 2,什么样的参数为volatile?

Item 2: auto语句中的类型推断

如何推断下面的代码中 auto 分别代表了什么类型呢?

1
2
3
auto x = 27;
const auto cx = x;
const auto& rx = x;

一般情况下,可以用模板函数中的类型推断来判断auto的结果。如:

1
2
3
4
5
6
7
8
9
10
11
template<typename T> 			// conceptual template for
void func_for_x(T param); // deducing x's type
func_for_x(27);

template<typename T> // conceptual template for
void func_for_cx(const T param); // deducing cx's type
func_for_cx(x);

template<typename T> // conceptual template for
void func_for_rx(const T& param); // deducing rx's type
func_for_rx(x);

根据 Item 1,我们可以将模板的类型推断分为三种:

  • Case 1: The type specifier is a pointer or reference, but not a universal reference.
  • Case 2: The type specifier is a universal reference.
  • Case 3: The type specifier is neither a pointer nor a reference.

这样,我们可以很自然地推断出下面的例子 auto 具体指代的内容:

1
2
3
4
5
6
7
8
9
10
auto x = 27; // case 3 (x is neither ptr nor reference)
const auto cx = x; // case 3 (cx isn't either)
const auto& rx = x; // case 1 (rx is a non-universal ref.)

auto&& uref1 = x; // x is int and lvalue,
// so uref1's type is int&
auto&& uref2 = cx; // cx is const int and lvalue,
// so uref2's type is const int&
auto&& uref3 = 27; // 27 is int and rvalue,
// so uref3's type is int&&

同样的,对于数组、函数,也有引用和非引用的区别

1
2
3
4
5
6
7
8
9
const char name[] = "R. N. Briggs";  // name's type is const char[13]

auto arr1 = name; // arr1's type is const char*
auto& arr2 = name; // arr2's type is const char (&)[13]

void someFunc(int, double); // someFunc is a function;
// type is void(int, double)
auto func1 = someFunc; // func1's type is void (*)(int, double)
auto& func2 = someFunc; // func2's type is void (&)(int, double)

如果用 auto 的对象是初始化列表,推断的结果也是初始化列表

1
2
3
int x = {27};
auto y = {27}; // type is std::initializer_list<int>

这也是 auto 和模板类型推断会不同的唯一地方:

1
2
3
4
5
6
7
auto x = { 11, 23, 9 }; // x's type is
// std::initializer_list<int>
template<typename T> // template with parameter
void f(T param); // declaration equivalent to
// x's declaration
f({ 11, 23, 9 }); // error! can't deduce type for T

只能像下面这样用模板推断 initializer_list:

1
2
3
4
5
template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); // T deduced as int, and initList's
// type is std::initializer_list<int>

另外,虽然C++14允许使用 'auto' 来推断函数返回类型或 lambda 函数参数,但这种用法是使用的模板类型推断。因此,若返回的是初始化列表并不能编译:

1
2
3
4
5
6
7
8
auto createInitList()
{
return { 1, 2, 3 }; // error: can't deduce type
} // for { 1, 2, 3 }

auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
resetV({ 1, 2, 3 }); // error! can't deduce type for { 1, 2, 3 }

Things to Remember

  • auto type deduction is usually the same as template type deduction, but auto type deduction assumes that a braced initializer represents a std::initial izer_list, and template type deduction doesn’t.
  • auto in a function return type or a lambda parameter implies template type deduction, not auto type deduction.

Item 3: 理解decltype

先来一些 no surprise 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int i = 0;         // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) is int
}; // decltype(Point::y) is int
Widget w; // decltype(w) is Widget
if (f(w)) … // decltype(f(w)) is bool

template<typename T> // simplified version of std::vector
class vector {
public:

T& operator[](std::size_t index);

};
vector<int> v; // decltype(v) is vector<int>

if (v[0] == 0) … // decltype(v[0]) is int&

在C++11中,decltype 主要用于函数返回类型(return type)基于参数类型(parameter types)的情况:

1
2
3
4
5
6
7
8
template<typename Container, typename Index> // works, but
auto authAndAccess(Container& c, Index i) // requires
-> decltype(c[i]) // refinement
{
authenticateUser();
return c[i];
}

在C++14中可以直接使用 auto 作为返回类型:

1
2
3
4
5
6
7
template<typename Container, typename Index> // C++14;
auto authAndAccess(Container& c, Index i) // not quite
{ // correct
authenticateUser();
return c[i]; // return type deduced from c[i]
}

值得注意的是,尽管 c[i] 一般返回的是引用类型,但由于模板类型推断是去引用的,所以下面的代码是错误的。

1
2
3
4
5
6
std::deque<int> d;

authAndAccess(d, 5) = 10; // authenticate user, return d[5],
// then assign 10 to it;
// this won't compile!

这里,函数 authAndAccess 返回的是一个右值(rvalue),C++ 中给右值赋值是被禁止的。

C++14可以 decltype(auto) 来实现返回与c[i]一致的类型:

1
2
3
4
5
6
7
8
template<typename Container, typename Index> // C++14; works,
decltype(auto) // but still
authAndAccess(Container& c, Index i) // requires
{ // refinement
authenticateUser();
return c[i];
}

decltype(auto)也可以用在变量的声明中:

1
2
3
4
5
6
7
8
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction:
// myWidget1's type is Widget
decltype(auto) myWidget2 = cw; // decltype type deduction:
// myWidget2's type is
// const Widget&

decltype 的 surprise 出现在右值容器上。用户可能只想得到临时容器中的拷贝,如:

1
2
3
4
5
std::deque<std::string> makeStringDeque(); // factory function
// make copy of 5th element of deque returned
// from makeStringDeque
auto s = authAndAccess(makeStringDeque(), 5);

这时用decltype(auto)就会得到对容器元素的引用,这是并不安全的。为了让 authAndAccess 能同时接受左值和右值,需要用上Universal reference:

1
2
3
4
template<typename Container, typename Index> // c is now a
decltype(auto) authAndAccess(Container&& c, // universal
Index i); // reference

书上还提到了forward,虽然不太了解还是放上来吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename Container, typename Index> // final
decltype(auto) // C++14
authAndAccess(Container&& c, Index i) // version
{
authenticateUser();
return std::forward<Container>(c)[i];
}

template<typename Container, typename Index> // final
auto // C++11
authAndAccess(Container&& c, Index i) // version
-> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}

变量名是个左值,对一个变量名字使用 decltype,会得到该变量的 declared type。但对于比变量名字更复杂的左值表达式,decltype 总是会得到一个左值引用。比如下面的这个奇怪的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
decltype(auto) f1()
{
int x = 0;

return x; // decltype(x) is int, so f1 returns int
}

decltype(auto) f2()
{
int x = 0;

return (x); // decltype((x)) is int&, so f2 returns int&
}

x 是一个类型为 int 的变量名,所以 f1 是 int 类型的。但 (x) 是比名字更复杂的左值表达式,所以 f2 是 int & 类型的。

Things to Remember

  • decltype almost always yields the type of a variable or expression without any modifications.
  • For lvalue expressions of type T other than names, decltype always reports a type of T&.
  • C++14 supports decltype(auto), which, like auto, deduces a type from its initializer, but it performs the type deduction using the decltype rules.

Item 4: 如何观测实际的类型推断

书中给出了三种方法:IDE 自动推断(getting type deduction information as you edit your code),编译中获取(getting it during compilation)和在运行中获取(getting it at runtime)。

Visual Studio 2015可以对相对简单的类型进行推断:

1
2
3
4
const int theAnswer = 42;
auto x = theAnswer; //把鼠标放在auto上就能看到推断的类型
auto y = &theAnswer;

在代码编译时,若出现 error,会给出相关的信息,这也能得到推断的类型,如:

1
2
3
4
5
template<typename T> // declaration only for TD;
class TD; // TD == "Type Displayer"
TD<decltype(x)> xType; // elicit errors containing
TD<decltype(y)> yType; // x's and y's types

由于TD并没有定义,会出现如下的错误:
error: aggregate 'TD<int> xType' has incomplete type and cannot be defined error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defineds

在函数运行时,你也可以使用函数typeid:

1
2
3
std::cout << typeid(x).name() << '\n'; 
std::cout << typeid(*x).name() << '\n';

然而,这些方法都或多或少有些问题,如VS中的类型推断可能不是很直白:

1
2
3
const std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *

有个Boost TypeIndex library(http://boost.com ),可以在std::type_info::name 和 IDEs没那么好用的情况下使用。

Things to Remember

  • Deduced types can often be seen using IDE editors, compiler error messages, and the Boost TypeIndex library.
  • The results of some tools may be neither helpful nor accurate, so an understanding of C++’s type deduction rules remains essential.