摘要訊息 : 一些 C++ 20 引入的小特性.
0. 前言
C++ 20 引入的絕大多數重要特性我們基本都介紹完了, 可以在 Jonny'Blog 中搜尋 C++ 20 閱覽這些文章. 還有一些小特性也是 C++ 20 引入的, 本文文章將通過羅列的方式介紹一部分特性.
1. 破壞性 operator new
如果一個類別具有柔性陣列的性質, 編碼器可能無法正確地進行解構 :
#include <vector>
template <typename T>
class inlined_array {
private:
std::size_t size;
public:
inlined_array() = delete;
T *data() noexcept {
return static_cast<T *>(this + 1);
}
static inlined_array *make(const std::vector<T> &v) {
const auto size {v.size()};
auto r {::operator new(sizeof(std::size_t) + size * sizeof(T))};
*static_cast<std::size_t *>(r) = size;
auto p {static_cast<T *>(static_cast<char *>(r) + static_cast<std::ptrdiff_t>(sizeof(std::size_t)))};
for(auto i {0}; i < size; ++i) {
new (p++) T {v[i]};
}
return static_cast<inlined_array *>(r);
}
};
若我們使用 inlined_array<T>::make
回傳的物件, 則必須使用 delete
運算子進行解構和記憶體回收. 但是 delete
運算子會回收 inlined_array<T>::size
之後的記憶體嗎? 也就是說, delete
運算子會選擇 ::operator delete(&inlined_array_object, sizeof inlined_array_object)
還是 ::operator delete (&inlined_array_object, sizeof inlined_array_object + sizeof(T) * inlined_array_object.size)
, 又或者乾脆呼叫 ::operator delete(&inlined_array_object)
?
在理想的情況下, 我們應該為 inlined_array
類別多載 operator delete
:
#include <vector>
template <typename T>
class inlined_array {
private:
std::size_t size;
public:
inlined_array() = delete;
T *data() noexcept {
return static_cast<T *>(this + 1);
}
static inlined_array *make(const std::vector<T> &v) {
const auto size {v.size()};
auto r {::operator new(sizeof(std::size_t) + size * sizeof(T))};
*static_cast<std::size_t *>(r) = size;
auto p {static_cast<T *>(static_cast<char *>(r) + static_cast<std::ptrdiff_t>(sizeof(std::size_t)))};
for(auto i {0}; i < size; ++i) {
new (p++) T {v[i]};
}
return static_cast<inlined_array *>(r);
}
static void operator delete(void *p) noexcept {
auto size {static_cast<inlined_array *>(p)->size};
::operator delete(p, sizeof(inlined_array) + sizeof(T) * size);
}
};
然而, 當運作 static_cast<inlined_array *>(p)->size
之前, 解構子已經被呼叫了. 此時, 再去存取成員 size
會導致未定行為.
再來看另一個例子, 這個例子中的類別將若干個物件放置在了類別之前 :
template <typename T>
class prestorage_array {
private:
std::size_t size;
public:
prestorage_array(std::size_t size);
void *operator new(std::size_t size, int n) {
auto prestorage_size {n * sizeof(T)};
auto r {static_cast<char *>(::operator new(size + prestorage_size) + static_cast<std::ptrdiff_t>(prestorage_size)};
return r;
}
void operator delete(void *p, std::size_t size) noexcept {
auto n {static_cast<prestorage_array *>(p)->size};
::operator delete(static_cast<char *>(p) - static_cast<std::ptrdiff_t>(n * sizeof(T)), n * sizeof(T) + size);
}
T &operator[](std::size_t n) noexcept {
return *(static_cast<T *>(this) - (this->size - n));
}
};
在 Code 2 中, 類別多載的 operator delete
同樣會導致未定行為. 為了解決這個問題, C++ 20 提案 P0722R3《Efficient sized delete for variable sized classes》為 C++ 20 引入了新的 operator delete
:
void T::operator delete(T *, std::destroying_delete_t);
void T::operator delete(T *, std::destroying_delete_t, std::align_val_t);
void T::operator delete(T *, std::destroying_delete_t, std::size_t);
void T::operator delete(T *, std::destroying_delete_t, std::size_t, std::align_val_t);
這四個 operator delete
被稱為破壞性 operator delete
(destroying operator delete
). 破壞性 operator delete
破壞了 delete
運算子的運作順序. 正常來說, delete
運算子的運算順序應該是首先呼叫類別的解構子, 然後再呼叫對應的 operator delete
. 若 delete
運算子遇到了破壞性 operator delete
, 那麼就不會再去呼叫類別的結構字, 而是直接呼叫破壞性 operator delete
. 這個時候, 類別的解構子需要由破壞性 operator delete
的實作者主動來呼叫. 從宣告中, 我們可以發現這些 operator delete
只能被放在類別之中.
例如在 Code 2 中, 我們會將 operator delete
修改為
template <typename T>
class prestorage_array {
private:
std::size_t size;
public:
prestorage_array(std::size_t size);
void *operator new(std::size_t size, int n) {
auto prestorage_size {n * sizeof(T)};
auto r {static_cast<char *>(::operator new(size + prestorage_size) + static_cast<std::ptrdiff_t>(prestorage_size)};
return r;
}
void operator delete(prestorage_array *p, std::destroying_delete_t, std::size_t size) noexcept {
auto n {p->size}; // OK
p->~prestorage_array(); // 主動呼叫類別解構子
::operator delete(static_cast<char *>(p) - static_cast<std::ptrdiff_t>(n * sizeof(T)), n * sizeof(T) + size);
}
T &operator[](std::size_t n) noexcept {
return *(static_cast<T *>(this) - (this->size - n));
}
};
值得一提的是, std::destroying_delete_t
是一個標籤類別, 方便編碼器區分是否使用破壞性 operator delete
, 依據此來改變 delete
的運作方式. 一般來說, 使用者不會產生型別為 std::destroying_delete_t
的物件, 也基本不會在除了 operator delete
之外的地方使用它.
2. __has_cpp_attribute
C++ 17 引入了 __has_include
(參考《C++ 17 特性合集 (一)》第 10 節), C++ 20 提案 P0941R2《Integrating feature-test macros into the C++ WD》為 C++ 帶來了 __has_cpp_attribute
以測試當前編碼器是否支援某個屬性 (參考《【C++ 11】Attribute》).
3. explicit(bool)
設有程式碼
#include <utility>
struct A {
A(int);
};
struct B {
explicit B(int);
};
template <typename T>
class C {
private:
T t;
public:
template <typename U>
C(U &&u) : t {std::forward<U>(u)} {}
};
void f(C<B>, A);
如果我們呼叫 f(1, A {1})
, 可以順利通過編碼. 值 1
通過 C(U &&)
這個建構子呼叫了 B(int)
這個建構子. 但是我們本意並不相讓 int
型別可以直接向 B
發生隱含型別轉化. 因此, C
的建構子設計是有問題的. 於是, 我們修改 C
的建構子, 通過 SFINAE 或者 Concept 來防止隱含型別轉化的直接發生 :
#include <utility>
#include <type_traits>
struct A {
A(int);
};
struct B {
explicit B(int);
};
template <typename T>
class C {
private:
T t;
public:
template <typename U>
C(std::enable_if_t<std::is_convertible_v<U, T>, U> &&u) : t {std::forward<U>(u)} {}
// template <typename U> requires std::is_convertible_v<U, T>
// C(U &&u) : t {std::forward<U>(u)} {}
template <typename U>
explicit C(U &&u) : t {std::forward<U>(u)} {}
};
void f(C<B>, A);
這樣, f(1, A {1})
這樣的呼叫就會導致編碼錯誤.
為了簡化上面的程式碼, C++ 20 提案 P0892R2《explicit(bool)》提出了 explicit(bool)
. 其中, explicit
接受的參數必須是 bool
型別的常數表達式, 不能存在隱含型別轉化. 當 explicit
中的引數為 true
的時候, 建構子才會被標識 explicit
. 這樣, Code 4-2 中的程式碼可以簡化為
#include <utility>
#include <type_traits>
struct A {
A(int);
};
struct B {
explicit B(int);
};
template <typename T>
class C {
private:
T t;
public:
template <typename U>
explicit(not std::is_convertible_v<U, T>) C(U &&u) : t(std::forward<U>(u)) {}
};
4. 有號數整型型別必須採用二補碼
C11 嚴格限制了有號數整型型別的表示方法 : 二補碼, 一補碼和帶號數值的表示. 但是 C++ 在這一方面的限制比 C 要寬鬆很多 : 只要是由純二進制計數系統來表示即可. 現代編碼器 (包括 Clang, GCC 和 MSVC) 都採用了二補碼的形式, 或者說現代大部分的電腦架構都基於二補碼. 當然, 確實存在一些不使用二補碼的裝置. 在這種情況下, 通過二補碼編碼的程式可能會被用於那些不採用二補碼的裝置上, 從而造成不相容的情況. C++ 20 提案 P1236R1《Signed Integers are Two's Complement》提出, 嚴格限制 C++ 的有號數整型型別的表示方法為二補碼, 其它編碼方式可以採用程式庫來輔助. 針對這樣的要求, P1236R1 提出了以下修改 :
- 在
bool
型別下, 嚴格定義0
表示false
,1
表示true
, 限制補齊並且規定其它值都是未定義; - 帶號數整型型別必須採用二補碼來表示;
- 若某個整型型別有 n 位元, 其中 m 位元用來表示值, 剩下的那些位元用於表示正負, 那麼 m 和 n 必須滿足 \displaystyle {n - m = 1};
- 整型型別不應像浮點數那樣有特殊值 (例如
NaN
); - 放棄從 C11 繼承用於奇偶校驗等用途的潛在填充位元;
- 除
bool
型別之外, 所有整型型別的值都唯一; - 列舉值在轉型之後的結果必須和列舉值的低層表示值相同 (例如列舉值
one
是int
型別, 表示值1
, 在轉型後的值不能和1
不同); x << y
的結果是嚴格的 x \times 2^{y} 的結果;x >> y
的結果是嚴格的 \frac {x}{2^{y}} 的結果;- 只要整型型別
T
存在位元補齊, 那麼std::has_unique_object_representation<T>
中的成員value
的值為false
.
雖然 P1236R1 提出了不少更改, 但是現有框架下的整數運算幾乎沒有受到影響. 因為絕大多數編碼器現有的處理方式就是符合上述更改的, 只是 P1236R1 認為原來的 C++ 標準對有號數整數的限制過於寬鬆, 並對 C++ 標準進行了嚴格規範.
5. char8_t
C++ 11 支援了 UTF-8, UTF-16 和 UTF-32, 並且增加了 char16_t
和 char32_t
這兩個型別. C++ 17 增加了 UTF-8 字元前綴 u8
, 但是 UTF-8 仍然借用 char
來表示. 而實際上, char
的具體編碼可能和作業系統和編碼器都有關, 所以 C++ 20 提案 P0482R6《char8_t
: A type for UTF-8 characters and strings》提出引入char8_t
, 並且放棄原來使用 char
來儲存 UTF-8 字元的方式. P0482R6 還為 C++ 標準樣板程式庫增添了和 char8_t
相關的設施, 如 std::u8string
和 std::u8string_view
等.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :