C++ Notes : Effective Modern C++ 笔记 —— 类型推断:条目 1,2,3,4

0. 准备

为了可以实时的知道推导结果是否正确,首先来定义一个显示推导结果的帮助函数。由于C++本身typeid(expr).name()的方法会去除类型的引用并且其结果可读性在gcc下不太直观。尽管可以自定义模板来解决这个问题,为了方便还是采取条目4中直接使用 boost 库中提供的boost::typeindex::typeid_with_cvr(保留constvolatile和引用信息)的方法来达到此目的。

// #include <boost/type_index.hpp> // required
#define ECHO_TYPE(T, U)                                             \
{                                                                   \
    using boost::typeindex::type_id_with_cvr;                       \
    std::cout.width(10), std::cout.setf(std::ios::left);            \
    std::cout << #U << " = "                                        \
              << type_id_with_cvr<T>().pretty_name()                \
              << std::endl;                                         \
}                                                                   \

#define ECHO_T_TYPE(T)          ECHO_TYPE(T, T)
#define ECHO_PARAM_TYPE(param)  ECHO_TYPE(decltype(param), param)
#define ECHO()                                                      \
{                                                                   \
    std::cout << std::string(30, '-') << std::endl;                 \
    ECHO_T_TYPE(T);                                                 \
    ECHO_PARAM_TYPE(param);                                         \
}                                                                   \

1. 模板推导

假定模板与调用函数定义如下:

template<typename T>
void f(paramType param);

f(expr);

TparamType就是由编译器在编译期根据expr推导得到。T的推导结果和paramType的声明类型有关。对于模板推导,有三种情况。

1. paramType声明为引用或指针(非通用引用T&&

  • 如果expr是引用,忽略引用部分
  • 然后匹配expr的类型和paramType对应的类型
// 这种情形的模板申明如下
template<typename T>
void f1(T &param) {
    ECHO();
}
// 调用函数
int v = 10;              // int
int &rv = v;             // int&
const int cv = v;        // const int
const int &crv = v;      // const int&
f1(v);                   // T is int, paramType is int&
f1(rv);                  // T is int, paramType is int&
f1(cv);                  // T is const int, paramType is const int&
f1(crv);                 // T is const int, paramType is const int&
// 实际运行结果
/*
------------------------------
T          = int
param      = int&
------------------------------
T          = int
param      = int&
------------------------------
T          = int const
param      = int const&
------------------------------
T          = int const
param      = int const&
*/

注意,T的类型推导受到paramType的声明影响。如果这里paramType声明为const T&,如果exprconst int&类型,首先去引用剩下const int,但是由于这里paramType声明为const,所以T不再需要constT的推导结果为intparamTypeconst int&。另外,这种情况下,paramType声明为左值引用,如果没有声明为const,是不可以推导类似于f(2)这种情况的。因为字面值常量2是纯右值,右值是不可以被非常量左值引用所引用的,除非是常量左值引用。即paramType声明为const T&,这时候f(2)推导出TintparamTypeconst int&

2. paramType声明为通用引用(T&&

  • 如果expr是左值,TparamType都被推断为左值引用T&。这是很特殊的一点!一方面,只有这种情况T才会被推断为引用类型;另一方面,尽管paramType被声明为右值引用类型T&&,其推导结果任为左值引用T&
  • 如果expr是右值,规则和 Case 1. 相同
// 模声明
template<typename T>
void f2(T &&param) {
    ECHO();
}
// 调用函数
f2(v);                  // T and paramType are both int&
f2(rv);                 // T and paramType are both int&
f2(cv);                 // T and paramType are both const int&
f2(crv);                // T and paramType are both const int&
f2(std::move(v));       // T is int, paramType is int&&
f2(std::move(rv));      // T is int, paramType is int&&
f2(std::move(cv));      // T is const int, paramType is int&&
f2(std::move(crv));     // T is const int, paramType is int&&
// 运行结果
/*
------------------------------
T          = int&
param      = int&
------------------------------
T          = int&
param      = int&
------------------------------
T          = int const&
param      = int const&
------------------------------
T          = int const&
param      = int const&
------------------------------
T          = int
param      = int&&
------------------------------
T          = int
param      = int&&
------------------------------
T          = int const
param      = int const&&
------------------------------
T          = int const
param      = int const&&
*/

3. paramType既不是引用也不是指针(pass-by-value)

  • 和前面一样,如果expr是引用,则忽略引用部分
  • 忽略引用部分后,如果exprconst或者volatileconstvolatile也忽略。
    引用constvolatile通通忽略。这是合理的,因为值传递是一份拷贝,及时实参是不可修改的,函数临时拷贝的一份不应当受此限制。
    注意,这里说的值传递,如果实参是指针类型,则值传递意味着传递指针地址本身。此时修饰指针指向地址所表示内容的const不可忽略,修饰指针指向的const可以忽略。即底层const不可忽略
// 模板定义
template<typename T>
void f3(T param) {
    ECHO();
}
// 函数调用
f3(cv);                 // both T and paramType are int
f3(crv);                // both T and paramType are int
const char *str1{"clion"};  // str1 is const char*
char *const str2{"clion"};  // str2 is char* const
f3(str1);               // both T and paramType is const char*
f3(str2);               // both T and paramType is char*
// 输出结果
/*
------------------------------
T          = int
param      = int
------------------------------
T          = int
param      = int
------------------------------
T          = char const*
param      = char const*
------------------------------
T          = char*
param      = char*
*/

数组实参

数组通常和指针联系在一起,所以数组形参通常可以声明为指针。比如,void func(int *arr);或者void func(int arr[]);。这两种形参,传递数组实参时数组都会退化为指针,所以其推导规则和指针是一样的。

但是,当数组形参声明为引用的时候,数组实参就不会退化为指针(通用引用也一样),这时候的推导规则就应当使用数组本身类型来推导。

int a[] = {1, 2, 3};    // int[3]

f1(a);                  // T is int[3], paramType is int(&)[3]
f2(a);                  // both T is int(&)[3]
f3(a);                  // both T and paramType are int*

这一特性还可以用来编译期获得数组维度

template<typename T, std::size_t N>
constexpr std::size_t getArrayLength(const T (&)[N]) {
    return N;
}
 auto len = getArrayLength(a);
// 编译期获得数组长度还有另外两种方法
auto len = sizeof(a) / sizeof(a[0]);            // 使用sizeof
auto len = std::extent<decltype(a)>::value;     // 使用std::extent

函数实参

函数实参和数组一样,也会退化为指针,推导规则和数组一样。

void funcToDeduce(int);             // type: void(int)

f1(funcToDeduce);                   // T is void(int), paramType is void(&)(int)
f2(funcToDeduce);                   // both T and paramType is void(&)(int)
f3(funcToDeduce);                   // both T and paramType is void(*)(int)

2. auto推导

auto推导和模板推导的规则基本一样。auto就相当于模板推导中的Tauto的推导完全可以套用模板推导规则,但是这里有一个特例。即从初始化列表推导时需要特殊注意,模板是不能从初始化列表推导的。

auto x1 = 1;            // int
auto x2(1);             // int
auto x3 = {1};          // std::initializer_list<int>
auto x4{1};             // int since C++17, std::initializer_list<int> before C++17
auto x5 = {1, 2};       // obviously std::initializer_list<int>
// auto x6{1, 2};       // FORBIDDEN
ECHO_PARAM_TYPE(x1);
ECHO_PARAM_TYPE(x2);
ECHO_PARAM_TYPE(x3);
ECHO_PARAM_TYPE(x4);
ECHO_PARAM_TYPE(x5);
// 输出
/*
x1         = int
x2         = int
x3         = std::initializer_list<int>
x4         = int
x5         = std::initializer_list<int>
*/

需要注意的是x4,2014年C++标准委员会接受了提案N3932,修改了auto对初始化列表的推导规则,所以C++17起或者在某些编译器上这里推导为int
要传初始化表实参给模板时,模板paramType需要声明为初始化表,即

template <typename T>
void f4(std::initializer_list<T> &&param) {
    ECHO();
}

f4({1});                // T is int, paramType is std::initializer_list<int>&&
// 输出
/*
------------------------------
T          = int
param      = std::initializer_list<int>&&
*/

另外,从C++14起 auto 可以推导函数返回类型,并且可以修饰lambda表达式的形参。但是作为这些用法时,auto的推导规则是按照模板推导规则来的。这个时候是无法推导列表初始化的

// auto generateInitList() {
//     return {1};              // Compile Error, decltype(auto) 也不行
// }

// std::vector<int> vec;
// auto resetVector = [&] (const auto& v) {
//     vec = v;
// };
// resetVector({1});            // 编译错误

3. decltype 推导

decltypeauto和模板推断不同,它将返回精确的类型(auto和模板在某些情况会去const和引用),ECHO_PARAM_TYPE()宏就是使用decltype来获得精确类型的。

有时候模板函数的返回值需要根据参数推导,在C++11中,可以使用decltype尾置返回类型(C++11不支持直接通过auto推导函数返回类型)

template <typename Container, typename Index>
auto getIndexAt1(Container& c, Index i) -> decltype(c[i]) {
    return c[i];
}

假设传入container为 std::vector<int>,那么返回类型被推导为int&
C++14开始允许直接使用auto从函数return语句推导返回类型。即无需尾置返回类型了。这时就容易出错了。

template <typename Container, typename Index>
auto getIndexAt2(Container& c, Index i) {
    return c[i];
}

传入container为std::vector<int>,推导返回类型为int(如果此时需要从返回值修改原始值,这样就做不到了)。这是因为这里的auto推导是根据模板推导规则来的,它会丢弃引用和const,这里需要使用decltype(auto)

template <typename Container, typename Index>
decltype(auto) getIndexAt3(Container& c, Index i) {
    return c[i];
}

传入container为std::vector<int>,推导返回类型为int&

PS: 更好的用法是使用完美转发,完美转发在后面的内容中提到,就此带过。

// perferct forward
template<typename Container, typename Index>
decltype(auto) getIndexAt4(Container &&c, Index i) {
    return std::forward<Container>(c)[i];
}

decltype(e)的推导规则:

  1. e是没有使用小括号()括起来的表达式,则推断结果就是其申明的类型
  2. eT类型的xvalue,则推断结果为T&&,右值引用
  3. eT类型的lvalue,则推断结果为T&,左值引用。(同时满足1的时候,优先1。所以通常得加括号)
  4. eT类型的prvalue,则推断结果为T。(字面值,临时变量)

NOTEint a = 1, b = 2;此时decltype((a + b))int而不是int&。因为虽然加了括号,a + b的结果是右值。

相关代码

猜你喜欢

转载自www.cnblogs.com/meow1234/p/emc_deducing_types.html