在c++中初始化對象的一種方法是聚合初始化,它允許使用花括号從多個值初始化:
struct Data
{
std::string name;
double value;
};
Data x{"test1", 6.778};
在c++ 17中聚合可以有基類,是以對于從其他類/結構派生的結構,允許初始化清單:
struct MoreData : Data
{
bool done;
};
MoreData y{{"test1", 6.778}, false};
如您所見,聚合初始化現在支援嵌套大括号,以便将值傳遞給基類的派生成員。
1. 擴充聚合初始化的意圖
如果沒有這個特性,則從另一個禁用的聚合初始化派生結構,是以必須定義一個構造函數:
struct Cpp14Data : Data
{
bool done;
Cpp14Data (const std::string& s, double d, bool b)
Data{s,d}, done{b}
{
}
};
Cpp14Data y{"test1", 6.778, false};
C++17提供了這個功能,文法略有不同,它很友善的将值傳遞到基類,如下:
MoreData y{{"test1", 6.778}, false};
2. 使用擴充聚合初始化
一個典型的應用是子類能夠使用清單初始化父類的C風格成員變量,子類可以添加額外的資料成員或操作。例如:
struct Data
{
const char* name;
double value;
};
struct PData : Data
{
bool critical;
void print() const
{
std::cout << '[' << name << ',' << value << "]\n";
}
};
PData y{{"test1", 6.778}, false};
y.print();
這裡,内嵌括号中的{"test1", 6.778}參數被傳遞給基類成員變量。
注意,您可以跳過初始值。在這種情況下,元素初始化為零(調用預設構造函數或使用0、false或nullptr初始化基本資料類型)。
例如:
PData a{}; // zero-initialize all elements
PData b{{"msg"}}; // 等同于 {{"msg",0.0},false}
PData c{{}, true}; // 等同于 {{nullptr,0.0},true}
PData d; //基本類型的值是未指定的
注意在這裡使用空花括号和完全不使用花括号的差別:
A. a zero-initializes all members,C風格字元串預設地為空指針(nullptr),double值初始化為0.0,bool标志初始化為false。
B. 變量d的定義所有其他成員都沒有初始化,并且具有未指定的值。
在visual studio 2019上測試結果如下:
在這裡我們把變量Data中的name類型修改為std::string,代碼如下:
例1:
#include <iostream>
#include <string>
struct Data
{
std::string name;
double value;
};
struct PData : Data
{
bool critical;
void print() const
{
std::cout << '[' << name << ',' << value << "]\n";
}
};
int main()
{
PData a{}; // zero-initialize all elements
PData b{ {"msg"} }; // same as {{"msg",0.0},false}
PData c{ {}, true }; // same as {{nullptr,0.0},true}
PData d; // values of fundamental types are unspecified
return 0;
}
此時變量a,b都會調用預設構造函數進行初始化。
結果如下:
還可以從非聚合類派生聚合。例如:
struct MyString : std::string
{
void print() const
{
if (empty())
{
std::cout << "<undefined>\n";
}
else
{
std::cout << c_str() << '\n';
}
}
};
MyString y{{"test1"}};
甚至可以從多個基類和/或聚合派生聚合:
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
然後你可以使用和初始化如下:
D<float> s{{"hello"}, {4.5,6.7}, "world"}; // OK since C++17
std::cout << s.data; // outputs: ”world”
std::cout << static_cast<std::string>(s); // outputs: ”hello”
std::cout << static_cast<std::complex<float>>(s); // outputs: (4.5,6.7)
在這裡内部初始化器清單按照基類聲明的順序傳遞給基類。這個新特性還有助于用很少的代碼定義重載lambdas(後續的文章會介紹)。
詳細代碼如下:
例2:
#include <iostream>
#include <string>
#include <complex>
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
int main(void)
{
D<float> s{ {"hello"}, {4.5,6.7}, "world" }; // OK since C++17
std::cout << s.data << std::endl; // outputs: ”world”
std::cout << static_cast<std::string>(s) << std::endl;; // outputs: ”hello”
std::cout << static_cast<std::complex<float>>(s) << std::endl; // outputs: (4.5,6.7)
return 0;
}
結果如下:
3. 聚合的定義
總而言之,由于c++ 17将聚合定義為:
a. 數組;
b. 或類類型(類、結構或聯合):
-- 沒有使用者定義的構造上述;
--沒有通過using聲明的繼承構造函數;
--沒有private或者protect的non-static資料成員;
--沒有virtual函數;
--沒有virtual或者private或者protected的基類。
為了能夠使用聚合,還需要在初始化期間不使用私有或受保護基類的成員或構造函數。
c++ 17還引入了一個新的類型trait is_aggregate<>來測試一個類型是否是一個聚合:
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
D<float> s{{"hello"}, {4.5,6.7}, "world"}; // OK since C++17
std::cout << std::is_aggregate<decltype(s)>::value; // outputs: 1 (true)
例 3:
#include <iostream>
#include <string>
#include <complex>
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
int main(void)
{
D<float> s{ {"hello"}, {4.5,6.7}, "world" }; // OK since C++17
const auto result = std::is_aggregate<decltype(s)>::value;
std::cout << result << std::endl;
return 0;
}
結果如下:
4. 向前不相容
注意下面的例子中不會相容c++14的文法。
例4:
#include<iostream>
struct Derived;
struct Base
{
friend struct Derived;
private:
Base()
{
std::cout << "Base constructor." << std::endl;
}
};
struct Derived : Base
{
};
int main()
{
Derived d1{}; // ERROR since C++17
Derived d2; // still OK (but might not initialize)
return 0;
}
C++17編譯會報如下錯誤:
C++14編譯輸出結果如下;
Base constructor.
Base constructor.
在c++ 17之前,Derived不是一個聚合。是以,Derived d1 {}調用派生類的編譯期自動生成的預設構造函數,該構造函數在預設情況下調用基類基的預設構造函數。雖然基類的預設構造函數是私有的,但是可以通過派生類的預設構造函數調用它,因為派生類被定義為friend類。
自從c++ 17起,Derived在這個示例中是一個聚合類,根本沒有一個隐式預設構造函數(沒有通過using聲明的繼承構造函數)。是以,初始化是一個聚合初始化,它不允許調用基類的私有構造函數。是否是基類的friend類并不重要。