Designated initializers 是 C++20 新的一種針對物件的成員資料初始化的方法。他基本上是 aggregate initialization(參考)的一種,然後算是透過加上引數名稱的形式、提高了物件初始化的可閱讀性。
像是如果有一個結構定義是:
struct STest { int iVal1 = 0; int iVal2 = 0; };
以往要初始化的話,基本上就是寫成下面的形式:
STest s{ 1,2 };
如果在成員資料多的時候,其實會很難去判斷哪個引述是對應到哪個成員;同時,也沒辦法略過前面的項目,只設定後面的資料。此外,如果日後有修改結構的話,也很容易出問題。
而透過新的 designated initializers 的寫法,這邊的初始化可以變得更明確、更有彈性;他的寫法基本上如下:
STest x{ .iVal1 = 1, .iVal2{2} };
基本上,就是可以透過 .iVal1 這樣的形式,直接指定是要設定哪個資料成員的值了;而這邊的「iVal1」就叫做「designator」,他只能用在非 static 的成員上,而指定值的方法可以用「=」,也可以用「{}」的形式。
透過這樣的寫法,就可以更明確地指定成員資料的值了;而這樣的語法,也可以跳過前面的成員,只指定後面的成員。
STest x{.iVal2 = 2 };
這樣就可以只設定 iVal2 的值、讓 iVal1 維持是預設值的 0。
在個人來看,這樣的寫法最大的好處,就是好閱讀、同時也有一定程度的彈性了。
而參考《Designated Initializers in C++20》的話,則可以看到一個可能還滿實際的例子:
struct Time { int hour; int minute; }; struct Date { Time t; int year; int month; int day; }; Date d{ .t {.hour = 10, .minute = 35 }, .year = 2050, .month = 5, .day = 10 };
在這種情境下,要使用 Date 這個結構,初始話就可以變得更明確了。
另一方面,這樣在調整定義的時候,也可以減少問題。
例如,假設有一天把前面的 STest 改成下面的樣子:
struct STest { int iVal0 = 0; int iVal1 = 0; int iVal2 = 0; };
也就是前面多了一個 iVal0。這個時候,前面本來結果一樣的寫法,現在雖然都還可以邊一,但是會產生出不同的結果:
STest s{ 1, 2 }; // 1,2,0 STest x{ .iVal1 = 1, .iVal2{2} }; // 0,1,2
在這個狀況下,使用 designated initializers 的寫法,應該是更為明確的!
同時,對應其他形式的修改(例如調整成員的順序),也是更能反映出問題的。
另外相對地,他在使用也有一些限制:
- 順序要按照宣告的順序
- 不能用在 static data member
- 只能用在 aggregate initialization
- 不能巢狀使用
所以,如果上面的例子寫成:
STest x{ .iVal2 = 2, .iVal1 = 1 };
這樣的話,就會因為順序和宣告的不符合,而無法正確編譯。
不過老實說,個人是覺得都給了變數名稱還要限制順序,感覺還滿蠢的…
而不能巢狀使用,則是指不能使用下面的寫法:
Date d2{ .t.hour = 10, .t.minute = 35, // ERROR!! .year = 2050, .month = 5, .day = 10 };
這樣的寫法,基本上還是 aggregate initialization 的一種形式,所以必須要符合 aggregate type 的條件才有機會使用。而在 C++20 的定義是:
- 陣列
- 類別(class / struct)
- 不包含 private、protected 的非靜態資料
- 沒有使用者定義的建構子(包含繼承來的)
- 沒有 virtual、private、protected 繼承
- 沒有 virtual 的成員函式
所以如果是一個有定義建構子的類別,就不能用 designated initializers 的寫法了。