C++11——自動類型推導

轉載來自:https://subingwen.cn/cpp/autotype/

在 C++11 中增加了很多新的特性,比如可以使用 auto 自動推導變量的類型,還能夠結合 decltype 來表示函數的返回值。使用新的特性可以讓我們寫出更加簡潔,更加現代的代碼。

1. auto
在 C++11 之前 auto 和 static 是對應的,表示變量是自動存儲的,但是非 static 的局部變量默認都是自動存儲的,因此這個關鍵字變得非常雞肋,在 C++11 中他們賦予了新的含義,使用這個關鍵字能夠像別的語言一樣自動推導出變量的實際類型。

1.1 推導規則
C++11 中 auto 並不代表一種實際的數據類型,只是一個類型聲明的 “佔位符”,auto 並不是萬能的在任意場景下都能夠推導出變量的實際類型,使用auto聲明的變量必須要進行初始化,以讓編譯器推導出它的實際類型,在編譯時將auto佔位符替換爲真正的類型。使用語法如下:

auto 變量名 = 變量值;
auto x = 3.14;      // x 是浮點型 double
auto y = 520;       // y 是整形 int
auto z = 'a';       // z 是字符型 char
auto nb;            // error,變量必須要初始化
auto double nbl;    // 語法錯誤, 不能修改數據類型   

 

不僅如此,auto 還可以和指針、引用結合起來使用也可以帶上 const、volatile 限定符,在不同的場景下有對應的推導規則,規則內容如下:

當變量不是指針或者引用類型時,推導的結果中不會保留 const、volatile 關鍵字
當變量是指針或者引用類型時,推導的結果中會保留 const、volatile 關鍵字

int temp = 110;
auto *a = &temp;    
auto b = &temp;        
auto &c = temp;        
auto d = temp;    

 

 

 這邊可以看到

a 是 int*

b 是int*

c 是int&

d 是int

 

    int tmp = 250;
    const auto a1 = tmp;
    auto a2 = a1;
    const auto& a3 = tmp;
    auto& a4 = a3;

 

 

 可以看到

a1 是 const int

a2卻看到const沒有了

a3是const int&

a4是const int&

1.2 auto的限制

auto 關鍵字並不是萬能的,在以下這些場景中是不能完成類型推導的

不能作爲函數參數使用。因爲只有在函數調用的時候纔會給函數參數傳遞實參,auto 要求必須要給修飾的變量賦值,因此二者矛盾。

int func(auto a, auto b) // error
{
    cout << "a: " << a << ", b: " << b << endl;
}

不能用於類的非靜態成員變量的初始化

class Test
{
    auto v1 = 0; // error
    static auto v2 = 0; // error,類的靜態非常量成員不允許在類內部直接初始化
    static const auto v3 = 10; // ok
}

不能使用 auto 關鍵字定義數組

        int array[] = { 1,2,3,4,5 }; // 定義數組
        auto t1 = array; // ok, t1被推導爲 int* 類型
        auto t2[] = array; // error, auto無法定義數組
        auto t3[] = { 1,2,3,4,5 }; // error, auto無法定義數組

無法使用 auto 推導出模板參數

template <typename T>
    struct Test {}

    int func()
    {
        Test<double> t;
        Test<auto> t1 = t; // error, 無法推導出模板類型
        return 0;
    }

1.3 auto 的應用

瞭解了 auto 的限制之後,我們就可以避開這些場景快樂的編程了,下面列舉幾個比較常用的場景:

用於STL的容器遍歷。

在 C++11 之前,定義了一個 stl 容器之後,遍歷的時候常常會寫出這樣的代碼:

#include <map>
int main()
{
    map<int, string> person;
    map<int, string>::iterator it = person.begin();
    for (; it != person.end(); ++it)
    {
        // do something
    }
    return 0;
}

可以看到在定義迭代器變量 it 的時候代碼是很長的,寫起來就很麻煩,使用了 auto 之後,就變得清爽了不少:

#include <map>
int main()
{
    map<int, string> person;
    // 代碼簡化
    for (auto it = person.begin(); it != person.end(); ++it)
    {
        // do something
    }
    return 0;
}

用於泛型編程,在使用模板的時候,很多情況下我們不知道變量應該定義爲什麼類型,比如下面的代碼:

#include <iostream>
#include <string>
using namespace std;

class T1
{
public:
    static int get()
    {
        return 10;
    }
};

class T2
{
public:
    static string get()
    {
        return "hello, world";
    }
};

template <class A>
void func(void)
{
    auto val = A::get();
    cout << "val: " << val << endl;
}

int main()
{
    func<T1>();
    func<T2>();
    return 0;
}

 

在這個例子中定義了泛型函數 func,在函數中調用了類 A 的靜態方法 get () ,這個函數的返回值是不能確定的如果不使用 auto,就需要再定義一個模板參數,並且在外部調用時手動指定 get 的返回值類型,具體代碼如下:

#include <iostream>
#include <string>
using namespace std;

class T1
{
public:
    static int get()
    {
        return 0;
    }
};

class T2
{
public:
    static string get()
    {
        return "hello, world";
    }
};

template <class A, typename B> // 添加了模板參數 B
void func(void)
{
    B val = A::get();
    cout << "val: " << val << endl;
}

int main()
{
    func<T1, int>(); // 手動指定返回值類型 -> int
    func<T2, string>(); // 手動指定返回值類型 -> string
    return 0;
}

 

2. decltype
在某些情況下,不需要或者不能定義變量,但是希望得到某種類型,這時候就可以使用 C++11 提供的 decltype 關鍵字了,它的作用是在編譯器編譯的時候推導出一個表達式的類型,語法格式如下:

decltype (表達式)
decltype 是 “declare type” 的縮寫,意思是 “聲明類型”。decltype 的推導是在編譯期完成的,它只是用於表達式類型的推導,並不會計算表達式的值。來看一組簡單的例子:

int a = 10;
decltype(a) b = 99; // b -> int
decltype(a+3.14) c = 52.13; // c -> double
decltype(a+b*c) d = 520.1314; // d -> double

 

可以看到 decltype 推導的表達式可簡單可複雜,在這一點上 auto 是做不到的,auto 只能推導已初始化的變量類型

2.1 推導規則
通過上面的例子我們初步感受了一下 decltype 的用法,但不要認爲 decltype 就這麼簡單,在它簡單的背後隱藏着很多的細節,下面分三個場景依次討論一下:

表達式爲普通變量或者普通表達式或者類表達式,在這種情況下,使用 decltype 推導出的類型和表達式的類型是一致的。

#include <iostream>
#include <string>
using namespace std;

class Test
{
public:
    string text;
    static const int value = 110;
};

int main()
{
    int x = 99;
    const int& y = x;
    decltype(x) a = x;
    decltype(y) b = x;
    decltype(Test::value) c = 0;

    Test t;
    decltype(t.text) d = "hello, world";

    return 0;
}

變量 a 被推導爲 int 類型
變量 b 被推導爲 const int & 類型
變量 c 被推導爲 const int 類型
變量 d 被推導爲 string 類型
表達式是函數調用,使用 decltype 推導出的類型和函數返回值一致。

 

#include <iostream>
#include <string>
using namespace std;

class Test
{
public:
    string text;
    static const int value = 110;
};
//函數聲明
int func_int(); // 返回值爲 int
int& func_int_r(); // 返回值爲 int&
int&& func_int_rr(); // 返回值爲 int&&
const int func_cint(); // 返回值爲 const int
const int& func_cint_r(); // 返回值爲 const int&
const int&& func_cint_rr(); // 返回值爲 const int&&
const Test func_ctest(); // 返回值爲 const Test

int main()
{
    //decltype類型推導
    int n = 100;
    decltype(func_int()) a = 0;
    decltype(func_int_r()) b = n;
    decltype(func_int_rr()) c = 0;
    decltype(func_cint()) d = 0;
    decltype(func_cint_r()) e = n;
    decltype(func_cint_rr()) f = 0;
    decltype(func_ctest()) g = Test();

    return 0;
}

 

 變量 a 被推導爲 int 類型

變量 b 被推導爲 int& 類型
變量 c 被推導爲 int&& 類型
變量 d 被推導爲 int 類型
變量 e 被推導爲 const int & 類型
變量 f 被推導爲 const int && 類型
變量 g 被推導爲 const Test 類型

函數 func_cint () 返回的是一個純右值(在表達式執行結束後不再存在的數據,也就是臨時性的數據)

對於純右值而言,只有類類型可以攜帶const、volatile限定符除此之外需要忽略掉這兩個限定符,因此推導出的變量 d 的類型爲 int 而不是 const int。

表達式是一個左值,或者被括號 ( ) 包圍使用 decltype 推導出的是表達式類型的引用(如果有 const、volatile 限定符不能忽略)。

#include <iostream>
#include <vector>
using namespace std;

class Test
{
public:
    int num;
};

int main() {
    const Test obj;
    //帶有括號的表達式
    decltype(obj.num) a = 0;
    decltype((obj.num)) b = a;
    //加法表達式
    int n = 0, m = 0;
    decltype(n + m) c = 0;
    decltype(n = n + m) d = n;
    return 0;
}

obj.num 爲類的成員訪問表達式,符合場景 1,因此 a 的類型爲 int
obj.num 帶有括號,符合場景 3,因此 b 的類型爲 const int&。
n+m 得到一個右值,符合場景 1,因此 c 的類型爲 int
n=n+m 得到一個左值 n,符合場景 3,因此 d 的類型爲 int&

2.2 decltype 的應用
關於 decltype 的應用多出現在泛型編程中。比如我們編寫一個類模板,在裏邊添加遍歷容器的函數,操作如下:

#include <list>
using namespace std;

template <class T>
class Container
{
public:
    void func(T& c)
    {
        for (m_it = c.begin(); m_it != c.end(); ++m_it)
        {
            cout << *m_it << " ";
        }
        cout << endl;
    }
private:
    ? ? ? m_it; // 這裏不能確定迭代器類型
};

int main()
{
    const list<int> lst;
    Container<const list<int>> obj;
    obj.func(lst);
    return 0;
}

在程序的第 17 行出了問題,關於迭代器變量一共有兩種類型:只讀(T::const_iterator)和讀寫(T::iterator),有了 decltype 就可以完美的解決這個問題了當 T 是一個 非 const 容器得到一個 T::iterator,當 T 是一個 const 容器時就會得到一個 T::const_iterator。

#include <list>
#include <iostream>
using namespace std;

template <class T>
class Container
{
public:
    void func(T& c)
    {
        for (m_it = c.begin(); m_it != c.end(); ++m_it)
        {
            cout << *m_it << " ";
        }
        cout << endl;
    }
private:
    decltype(T().begin()) m_it; // 這裏不能確定迭代器類型
};

int main()
{
    const list<int> lst{ 1,2,3,4,5,6,7,8,9 };
    Container<const list<int>> obj;
    obj.func(lst);
    return 0;
}#include <list>
#include <iostream>
using namespace std;

template <class T>
class Container
{
public:
    void func(T& c)
    {
        for (m_it = c.begin(); m_it != c.end(); ++m_it)
        {
            cout << *m_it << " ";
        }
        cout << endl;
    }
private:
    decltype(T().begin()) m_it; // 這裏不能確定迭代器類型
};

int main()
{
    const list<int> lst{ 1,2,3,4,5,6,7,8,9 };
    Container<const list<int>> obj;
    obj.func(lst);
    return 0;
}

decltype(T().begin()) 這種寫法在 vs2017/vs2019 下測試可用完美運行。

3. 返回類型後置
在泛型編程中,可能需要通過參數的運算來得到返回值的類型,比如下面這個場景:

#include <iostream>
using namespace std;
// R->返回值類型, T->參數1類型, U->參數2類型
template <typename R, typename T, typename U>
R add(T t, U u)
{
    return t + u;
}

int main()
{
    int x = 520;
    double y = 13.14;
    // auto z = add<decltype(x + y), int, double>(x, y);
    auto z = add<decltype(x + y)>(x, y); // 簡化之後的寫法
    cout << "z: " << z << endl;
    return 0;
}

關於返回值,從上面的代碼可以推斷出和表達式 t+u 的結果類型是一樣的,因此可以通過通過 decltype 進行推導,關於模板函數的參數 t 和 u 可以通過實參自動推導出來,因此在程序中就也可以不寫。雖然通過上述方式問題被解決了,但是解決方案有點過於理想化,因爲對於調用者來說,是不知道函數內部執行了什麼樣的處理動作的。

因此如果要想解決這個問題就得直接在 add 函數身上做文章,先來看第一種寫法:

template <typename T, typename U>
decltype(t + u) add(T t, U u)
{
    return t + u;
}

當我們在編譯器中將這幾行代碼改出來後就直接報錯了因此 decltype 中的 t 和 u 都是函數參數,直接這樣寫相當於變量還沒有定義就直接用上了,這時候變量還不存在,有點心急了。

在C++11中增加了返回類型後置語法,說明白一點就是將decltype和auto結合起來完成返回類型的推導。其語法格式如下:

// 符號 -> 後邊跟隨的是函數返回值的類型
auto func(參數1, 參數2, ...) -> decltype(參數表達式)

通過對上述返回類型後置語法代碼的分析,得到結論:auto 會追蹤 decltype() 推導出的類型因此上邊的 add() 函數可以做如下的修改:

#include <iostream>
using namespace std;

template <typename T, typename U>
// 返回類型後置語法
auto add(T t, U u) -> decltype(t + u)
{
    return t + u;
}

int main()
{
    int x = 520;
    double y = 13.14;
    // auto z = add<int, double>(x, y);
    auto z = add(x, y); // 簡化之後的寫法
    cout << "z: " << z << endl;
    return 0;
}

爲了進一步說明這個語法,我們再看一個例子:

#include <iostream>
using namespace std;

int& test(int& i)
{
    return i;
}

double test(double& d)
{
    d = d + 100;
    return d;
}

template <typename T>
// 返回類型後置語法
auto myFunc(T& t) -> decltype(test(t))
{
    return test(t);
}

int main()
{
    int x = 520;
    double y = 13.14;
    // auto z = myFunc<int>(x);
    auto z = myFunc(x); // 簡化之後的寫法
    cout << "z: " << z << endl;

    // auto z = myFunc<double>(y);
    auto z1 = myFunc(y); // 簡化之後的寫法
    cout << "z1: " << z1 << endl;
    return 0;
}

在這個例子中,通過 decltype 結合返回值後置語法很容易推導出來 test(t) 函數可能出現的返回值類型,並將其作用到了函數 myFunc() 上。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章