C++17結構化綁定

動機

std::map<K, V>insert方法返回std::pair<iterator, bool>,兩個元素分別是指向所插入鍵值對的迭代器與指示是否新插入元素的布爾值,而std::map<K, V>::iterator解引用又獲得鍵值對std::pair<const K, V>。在一個涉及std::map的算法中,有可能出現大量的firstsecond,讓人不知所措。html

#include <iostream>
#include <map>

int main()
{
    typedef std::map<int, int> Map;
    Map map;
    std::pair<Map::iterator, bool> result = map.insert(Map::value_type(1, 2));
    if (result.second)
        std::cout << "inserted successfully" << std::endl;
    for (Map::iterator iter = map.begin(); iter != map.end(); ++iter)
        std::cout << "[" << iter->first << ", " << iter->second << "]" << std::endl;
}

C++11標準庫添加了std::tie,用若干引用構造出一個std::tuple,對它賦以std::tuple對象能夠給其中的引用一一賦值(二元std::tuple能夠由std::pair構造或賦值)。std::ignore是一個佔位符,所在位置的賦值被忽略。ios

#include <iostream>
#include <map>
#include <utility>

int main()
{
    std::map<int, int> map;
    bool inserted;
    std::tie(std::ignore, inserted) = map.insert({1, 2});
    if (inserted)
        std::cout << "inserted successfully" << std::endl;
    for (auto&& kv : map)
        std::cout << "[" << kv.first << ", " << kv.second << "]" << std::endl;
}

可是這種方法仍遠不完美,由於:算法

  • 變量必須事先單獨聲明,其類型都需顯式表示,沒法自動推導;express

  • 對於默認構造函數執行零初始化的類型,零初始化的過程是多餘的;編程

  • 也許根本沒有可用的默認構造函數,如std::ofstream數組

爲此,C++17引入告終構化綁定(structured binding)。ide

#include <iostream>
#include <map>

int main()
{
    std::map<int, int> map;
    auto&& [iter, inserted] = map.insert({1, 2});
    if (inserted)
        std::cout << "inserted successfully" << std::endl;
    for (auto&& [key, value] : map)
        std::cout << "[" << key << ", " << value << "]" << std::endl;
}

結構化綁定這一語言特性在提議的階段曾被稱爲分解聲明(decomposition declaration),後來又被改回結構化綁定。這個名字想強調的是,結構化綁定的意義重在綁定而非聲明。函數

語法

結構化綁定有三種語法:優化

attr(optional) cv-auto ref-operator(optional) [ identifier-list ] = expression;
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] { expression };
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] ( expression );

其中,attr(optional)爲可選的attributescv-auto爲可能有constvolatile修飾的autoref-operator(optional)爲可選的&&&identifier-list爲逗號分隔的標識符,expression爲單個表達式。spa

另外再定義initializer= expression{ expression }( expression ),換言之上面三種語法有統一的形式attr(optional) cv-auto ref-operator(optional) [ identifier-list ] initializer;

整個語句是一個結構化綁定聲明,標識符也稱爲結構化綁定(structured bindings),不過兩處「binding」的詞性不一樣。

順帶一提,C++20中volatile的許多用法都被廢棄了。

行爲

結構化綁定有三類行爲,與上面的三種語法之間沒有對應關係。

第一種狀況,expression是數組,identifier-list的長度必須與數組長度相等。

第二種狀況,對於expression的類型Estd::tuple_size<E>是一個完整類型,則稱E爲類元組(tuple-like)類型。在STL中,std::arraystd::pairstd::tuple都是這樣的類型。此時,identifier-list的長度必須與std::tuple_size<E>::value相等,每一個標識符的類型都經過std::tuple_element推導出(具體見後文),用成員get<I>()get<I>(e)初始化。顯然,這些標準庫設施是與語言核心綁定的。

第三種狀況,E是非union類類型,綁定非靜態數據成員。全部非靜態數據成員都必須是public訪問屬性,所有在E中,或所有在E的一個基類中(即不能分散在多個類中)。identifier-list按照類中非靜態數據成員的聲明順序綁定,數量相等。

應用

結構化綁定擅長處理純數據類型,包括自定義類型與std::tuple等,給實例的每個字段分配一個變量名:

#include <iostream>

struct Point
{
    double x, y;
};

Point midpoint(const Point& p1, const Point& p2)
{
    return { (p1.x + p2.x) / 2, (p1.y + p2.y) / 2 };
}

int main()
{
    Point p1{ 1, 2 };
    Point p2{ 3, 4 };
    auto [x, y] = midpoint(p1, p2);
    std::cout << "(" << x << ", " << y << ")" << std::endl;
}

配合其餘語法糖,現代C++代碼能夠很優雅:

#include <iostream>
#include <map>

int main()
{
    std::map<int, int> map;
    if (auto&& [iter, inserted] = map.insert({ 1, 2 }); inserted)
        std::cout << "inserted successfully" << std::endl;
    for (auto&& [key, value] : map)
        std::cout << "[" << key << ", " << value << "]" << std::endl;
}

利用結構化綁定在類元組類型上的行爲,咱們能夠改變數據類型的結構化綁定細節,包括類型轉換、是否拷貝等:

#include <iostream>
#include <string>
#include <utility>

class Transcript { /* ... */ };

class Student
{
public:
    const char* name;
    Transcript score;
    std::string getName() const { return name; }
    const Transcript& getScore() const { return score; }
    template<std::size_t I>
    decltype(auto) get() const
    {
        if constexpr (I == 0)
            return getName();
        else if constexpr (I == 1)
            return getScore();
        else
            static_assert(I < 2);
    }
};

namespace std
{
template<>
struct tuple_size<Student>
    : std::integral_constant<std::size_t, 2> { };

template<>
struct tuple_element<0, Student> { using type = decltype(std::declval<Student>().getName()); };

template<>
struct tuple_element<1, Student> { using type = decltype(std::declval<Student>().getScore()); };
}

int main()
{
    std::cout << std::boolalpha;
    Student s{ "Jerry", {} };
    const auto& [name, score] = s;
    std::cout << name << std::endl;
    std::cout << (&score == &s.score) << std::endl;
}

Student是一個數據類型,有兩個字段namescorename是一個C風格字符串,它大概是從C代碼繼承來的,我但願客戶能用上C++風格的std::stringscore屬於Transcript類型,表示學生的成績單,這個結構比較大,我但願能傳遞const引用以免沒必要要的拷貝。爲此,我寫明瞭三要素:std::tuple_sizestd::tuple_elementget。這種機制給告終構化綁定很強的靈活性。

細節

#include <iostream>
#include <utility>
#include <tuple>

int main()
{
    std::pair pair{ 1, 2.0 };
    int number = 3;
    std::tuple<int&> tuple(number);
    const auto& [i, f] = pair;
    //i = 4; // error
    const auto& [ri] = tuple;
    ri = 5;
}

若是結構化綁定i被聲明爲const auto&,對應的類型爲int,那麼它應該是個const int&吧?i = 4;出錯了,看起來正是如此。可是如何解釋ri = 5;是合法的呢?

這個問題須要系統地從頭談起。先引入一個名字eE爲其類型:

  • expression是數組類型A,且ref-operator不存在時,Ecv A,每一個元素由expression中的對應元素拷貝(= expression)或直接初始化({ expression }( expression )

  • 不然,至關於定義eattr cv-auto ref-operator e initializer;

也就是說,方括號前面的修飾符都是做用於e的,而不是那些新聲明的變量。至於爲何第一條會獨立出來,這是由於在標準C++中第二條的形式不能用於數組拷貝。

而後分三種狀況討論:

  • 數組情形,ET的數組類型,則每一個結構化綁定都是指向e數組中元素的左值;被引類型(referenced type)爲T

    ——結構化綁定是左值,不是左值引用:int array[2]{ 1, 2 }; auto& [i, j] = array; static_assert(!std::is_reference_v<decltype(i)>);

  • 類元組情形,若是e是左值引用,則e是左值(lvalue),不然是消亡值(xvalue);記Tistd::tuple_element<i, E>::type,則結構化綁定vi的類型是Ti的引用;當get返回左值引用時是左值引用,不然是右值引用;被引類型爲Ti

    ——decltype對結構化綁定有特殊處理,產生被引類型,在類元組情形下結構化綁定的類型與被引類型是不一樣的;

  • 數據成員情形,與數組相似,設數據成員mi被聲明爲Ti類型,則結構化綁定的類型是指向cv Ti的左值(一樣不是左值引用);被引類型爲cv Ti

至此,我想「結構化綁定」的意義已經明確了:標識符老是綁定一個對象,該對象是另外一個對象的成員(或數組元素),後者或是拷貝或是引用(引用不是對象,意會便可)。與引用相似,結構化綁定都是既有對象的別名(這個對象多是隱式的);與引用不一樣,結構化綁定不必定是引用類型。

(不理解的話能夠參考N4659 11.5節,儘管你極可能會更加看不懂……)

如今能夠解釋riconst的現象了:編譯器先建立了變量const auto& e = tuple;Econst std::tuple<int&>&std::tuple_element<0, E>::typeint&std::get<0>(e)一樣返回int&,故riint&類型。

在面向底層的C++編程中經常使用union和位域(bit field),結構化綁定支持這樣的數據成員。若是類有union類型成員,它必須是命名的,綁定的標識符的類型爲該union類型的左值;若是有未命名的union成員,則這個類不能用於結構化綁定。

C++中不存在位域的指針和引用,但結構化綁定能夠是指向位域的左值:

#include <iostream>

struct BitField
{
    int f1 : 4;
    int f2 : 4;
    int f3 : 4;
};

int main()
{
    BitField b{ 1, 2, 3 };
    auto& [f1, f2, f3] = b;
    f2 = 4;
    auto print = [&] { std::cout << b.f1 << " " << b.f2 << " " << b.f3 << std::endl; };
    print();
    f2 = 21;
    print();
}

程序輸出:

1 4 3
1 5 3

f2的功能就像位域的引用同樣,既能寫回原值,又不會超出位域的範圍。

還有一些語法細節,好比get的名字查找、std::tuple_size<E>沒有valueexplicit拷貝構造函數等,除非是深挖語法的language lawyer,在實際開發中沒必要糾結(上面這一堆已經能夠算language lawyer了吧)。

侷限

以上代碼示例應該已經囊括了全部類型的結構化綁定應用,你能想象到的其餘語法都是錯的,包括但不限於:

  • std::initializer_list<T>初始化;

    由於std::initializer_list<T>的長度是動態的,但結構化綁定的標識符數量是靜態的。

  • 用列表初始化——auto [x,y,z] = {1, "xyzzy"s, 3.14159};

    這至關於聲明瞭三個變量,但結構化綁定的意圖在於綁定而非聲明。

  • 不聲明而直接綁定——[iter, success] = mymap.insert(value);

    這至關於用std::tie,因此請繼續用std::tie。另外,由[開始可能與attributes混淆,給編譯器和編譯器設計者帶來壓力。

  • 指明結構化綁定的修飾符——auto [& x, const y, const& z] = f();

    一樣是脫離告終構化綁定的意圖。若是須要這樣的功能,或者一個個定義變量,或者手動寫上三要素。

  • 指明結構化綁定的類型——SomeClass [x, y] = f();auto [x, std::string y] = f();

    第一種可用auto [x, y] = SomeClass{ f() };代替;第二種同上一條。

  • 顯式忽略一個結構化綁定——auto [x, std::ignore, z] = f();

    消除編譯器警告是一個理由,可是auto [x, y, z] = f(); (void)y;亦可。這還涉及一些語言問題,請移步P0144R2 3.8節。

  • 標識符嵌套——std::tuple<T1, std::pair<T2, T3>, T4> f(); auto [ w, [x, y], z ] = f();

    多寫一行吧。[一樣可能與attributes混淆。

以上語法都沒有歸入C++20標準,不過可能在未來成爲C++語法的擴展。

延伸

C++17的新特性不是孤立的,與結構化綁定相關的有:

相關文章
相關標籤/搜索