最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++20新增屬性[[no_unique_address]]詳解(最新整理)

 更新時間:2025年09月10日 08:49:33   作者:apocelipes  
[[no_unique_address]]讓空類型的類數(shù)據(jù)成員有機會不再占用額外的內存空間,從而減輕了因為地址規(guī)定帶來的性能影響,同時還讓空基類優(yōu)化代碼得到了簡化的機會,本文給大家詳細介紹C++20新增屬性[[no_unique_address]],感興趣的朋友一起看看吧

有一個古老的c++問題:struct Empty{}; sizeof(Empty); 請問Empty的大小是多少。

很多新手會回答0,但稍有經(jīng)驗的開發(fā)者會說出正確答案,大小至少是1字節(jié)。

這看起來很奇怪,但這是語言規(guī)范決定的:c++要求同一類型的不同實例對象必須擁有完全不同的地址,如果Empty的大小是0,那么想象一下一個元素類型是Empty的數(shù)組,這個數(shù)組的連續(xù)存儲空間里很可能不同的Empty會重疊在一起,從而導致它們違反前面對于擁有不同地址的規(guī)定。最簡單最省事的做法就是讓這種看起來大小應該為0的類型占據(jù)一字節(jié)的內存,從而確保每個實例都有獨立的地址。而且語言規(guī)范也是要求這樣去做的,它要求所有零大小的類型除了位域都必須占至少一字節(jié)的內存。

這么做當然帶來了很多弊端,所以c++20新增了屬性[[no_unique_address]]來解決問題。

不過在介紹這個屬性之前,我們還得回顧一點基礎知識。

基礎回顧

c++的知識是一環(huán)套一環(huán)的,所以基礎回顧環(huán)節(jié)少不了。我們需要回顧三個小知識點:什么是空類型、什么是空基類優(yōu)化、空類型對內存對齊的影響。

首先回顧的是“空類型是什么”。

空類型,或者用語言規(guī)范里的叫法“zero size”,是指那些符合標準布局的、沒有虛基類虛函數(shù)、沒有非靜態(tài)數(shù)據(jù)成員的類型。如果存在繼承關系,則類型的每一層繼承關系上涉及的類型也都必須符合前面提到的條件,這樣的類型可以被視作是空類型。union不在此范圍之內。

簡單的說,下面三個類都可以被認為是空的:

struct A {
    static constexpr int i = 0; // 這是靜態(tài)數(shù)據(jù)成員,不影響類型為zero size
};
struct B {};
struct C: A {}; // 自己和基類都符合要求
int main()
{
    static_assert(std::is_empty_v<A>);
    static_assert(std::is_empty_v<B>);
    static_assert(std::is_empty_v<C>);
}

std::is_empty是c++11新增的用于判斷類型是否是zero size的接口。我們可以看到,沒有非靜態(tài)數(shù)據(jù)成員沒有虛函數(shù)且基類也符合同樣條件的類型都會被認為是空類型。

概念還是很容易理解的,不過標準并沒有把話說死,在后面標準緊接著指出任何編譯器覺得應該是空類型的東西也可以算作空類型。換句話說除了標準規(guī)定的少數(shù)情況,還有不少類型是否為空是具體平臺和編譯器共同影響的。

第二個要回顧的是“空類型對內存對齊的影響”。在復習空基類優(yōu)化之前我們需要知道優(yōu)化的動機,而動機來自于空類型對內存對齊的影響。

我們現(xiàn)在都知道因為c++對象地址的限制,空類型需要占用至少一字節(jié)的內存。這會讓程序付出代價:

struct Empty {};
struct A {
    long number;
    Empty e;
};
static_assert(sizeof(A) > sizeof(long));

A的大小至少為2個long類型的大小。為什么呢,因為c++有內存對齊的規(guī)則,類的對齊長度以所有非靜態(tài)數(shù)據(jù)成員中對齊長度最大的為準,這里我們有兩個非靜態(tài)數(shù)據(jù)成員,number和e,number的長度是sizeof(long),而它的對齊長度要求也是sizeof(long),e的長度和對齊要求都是1,sizeof(long)一定大于1,所以最后類型A要求每個字段都以sizeof(long)為基準進行對齊,作為最后一個字段的e,前面的字段number正好有一個long類型那么長,而自己后面又沒有其他字段,按對齊要求這時候需要在自己后面填充sizeof(long) - 1個字節(jié)的填充物。最后A的整體大小會是兩個long那么大。

實際上我們用不到Empty占用的內存里的內容,通常我們使用空類型是為了利用其類方法或者靜態(tài)數(shù)據(jù),但卻要為了這一字節(jié)付出內存占用上的代價。類型變成兩倍大意味著高速緩存里能存下的同類型數(shù)據(jù)至少減少一半,對于頻繁訪問這類數(shù)據(jù)的程序來說這是顯著的性能損失。

c++為了踐行“不支付不必要的運行時代價”,提出了EBO——空基類優(yōu)化(Empty Base Optimization)這一方案。

空基類優(yōu)化,是指當基類為空類型,派生類的第一個非靜態(tài)數(shù)據(jù)成員的類型和基類不一樣,繼承不是虛擬繼承的時候,這個空類型的基類可以不占用任何存儲空間。

舉個例子,還是前面的A:

struct Empty {};
struct A : Empty {
    long number;
};
static_assert(sizeof(A) == sizeof(long))

正常情況下基類也需要在派生類的內存空間內占據(jù)一部分地盤,但因為空基類優(yōu)化,這一字節(jié)的占用就免除了??栈悆?yōu)化也適用于多繼承:

struct Empty1 {};
struct Empty2 {};
struct A : Empty1, Empty2 {
    long number;
};
static_assert(sizeof(A) == sizeof(long))

通過繼承,我們也可以復用作為基類的空類型的靜態(tài)數(shù)據(jù)和類方法,同時又不用支付存儲的代價。

對于不滿足要求的類型,比如第一個數(shù)據(jù)成員的類型和基類相同,這時候空基類優(yōu)化就不生效了:

struct Empty {};
struct A: Empty {
    Empty e;
};
static_assert(sizeof(A) > sizeof(Empty));

A至少有兩個Empty那么大。因為在一部分平臺上基類的內存是緊挨著派生類的數(shù)據(jù)成員的,如果第一個數(shù)據(jù)成員的類型和基類相同,那么繼續(xù)應用空基類優(yōu)化就會導致基類和第一個數(shù)據(jù)成員發(fā)生重疊(基類的大小是0對其取地址通常會得到和派生類或者派生類數(shù)據(jù)成員相同的地址),這違反了c++對于同類型的不同對象地址必須不同的規(guī)定。

空基類優(yōu)化在標準庫里用的很多,比如Hasher、各種迭代器以及allocator,都是使用了空基類優(yōu)化來復用方法同時減小存儲負擔的。

另外還有一個比較知名的空基類優(yōu)化應用:compressed_pair,這是std::pair的變體,它在元素為空類型的時候可以不占用額外的內存,原理就是利用了空基類優(yōu)化。這種容器常見的第三方c++模板庫中都有提供,比如boost。

新屬性no_unique_address

空基類優(yōu)化看似解決了問題,然而繼承本身會引來新的問題。

繼承最大的問題在于派生類和基類的關系是is-a,即派生類從分類上是基類的某種延伸或者說派生類和基類直接有著相似的結構和操作方法。但如果我們只是想復用空類型中的方法或者干脆為了避免內存占用而使用空基類優(yōu)化,則會打破這種is-a關系。

考慮一下上一節(jié)說到的compressed_pair,再能利用no_unique_address之前它的實現(xiàn)是這樣的:

template <class _T1, class _T2>
class compressed_pair : private __compressed_pair_elem<_T1, 0>, private __compressed_pair_elem<_T2, 1> {
public:
  // NOTE: This static assert should never fire because __compressed_pair
  // is *almost never* used in a scenario where it's possible for T1 == T2.
  // (The exception is std::function where it is possible that the function
  //  object and the allocator have the same type).
  static_assert(
      (!is_same<_T1, _T2>::value),
      "__compressed_pair cannot be instantiated when T1 and T2 are the same type; "
      "The current implementation is NOT ABI-compatible with the previous implementation for this configuration");
  using _Base1 _LIBCPP_NODEBUG = __compressed_pair_elem<_T1, 0>;
  using _Base2 _LIBCPP_NODEBUG = __compressed_pair_elem<_T2, 1>;
  ...
};

__compressed_pair_elem是元素的包裝器,用來提供元素的訪問方法,以及在元素大小是0的時候讓自己的大小也為0,方便利用空基類優(yōu)化:

template <class _Tp, int _Idx, bool _CanBeEmptyBase = is_empty<_Tp>::value && !__libcpp_is_final<_Tp>::value>
struct __compressed_pair_elem {
  using _ParamT         = _Tp;
  using reference       = _Tp&;
  using const_reference = const _Tp&;
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __compressed_pair_elem(__default_init_tag) {}
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __compressed_pair_elem(__value_init_tag) : __value_() {}
  ...
  其他一些構造函數(shù),這里省略
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 reference __get() _NOEXCEPT { return __value_; }
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR const_reference __get() const _NOEXCEPT { return __value_; }
private:
  _Tp __value_;
};
// 注意下面這個為了對象大小是0的部分特化模板
template <class _Tp, int _Idx>
struct __compressed_pair_elem<_Tp, _Idx, true> : private _Tp {
  using _ParamT         = _Tp;
  using reference       = _Tp&;
  using const_reference = const _Tp&;
  using __value_type    = _Tp;
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __compressed_pair_elem() = default;
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __compressed_pair_elem(__default_init_tag) {}
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __compressed_pair_elem(__value_init_tag) : __value_type() {}
  其他一些構造函數(shù),這里省略
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 reference __get() _NOEXCEPT { return *this; }
  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR const_reference __get() const _NOEXCEPT { return *this; }
  // 注意這里,沒有任何數(shù)據(jù)成員,所以這個模板類的實例大小也是零,這個模板實例化出來的都是空類型
};

對于這些代碼,最直觀的感受就是長。對于模板用的不多的開發(fā)者來說這東西還會沾點難懂。但最重要的問題在于這一繼承關系闡述了這樣一個情況:pair是(is-a)一種pair自己的元素。很荒誕,

鑒于利用空基類優(yōu)化的代碼又長又復雜,還會違背繼承關系的原則,c++20接受了[[no_unique_address]]的提案,提供了一種不利用繼承同時又能讓不同類型的實例對象內存空間發(fā)生折疊的技術。

顧名思義,被[[no_unique_address]]修飾的東西可以沒有自己獨立的地址。具體來說這個屬性只能用在類的非靜態(tài)數(shù)據(jù)成員上,且根據(jù)字段是否是空類型會有不同的效果:

  • 如果是空類型,則這個字段可以和其他的類非靜態(tài)數(shù)據(jù)成員或者基類的內存空間重疊在一起,也就是這個字段本身不再占用內存,對這個字段取地址也會得到類的其他數(shù)據(jù)成員或者基類的地址。
  • 如果不為空,則這個字段后面因為內存對齊留下的空間可以被其他類成員利用。

對于非空類型來說,這個屬性沒有什么明顯的效果,因為目前只要相鄰的字段大小和對齊合適,就會自動利用前一個字段因為對齊而留下的空間。這個屬性只是有限度的放寬了“相鄰”這個限制,但類的成員還有offset偏移量這個限制需要遵守,所以很難在非空類型字段上看到這個屬性帶來的影響。

而對于空類型,這個屬性的影響就大了,舉個例子:

struct Empty {};
struct A {
    long number;
    [[no_unique_address]] Empty e;
};
static_assert(sizeof(A) == sizeof(long));
#include <cstddef>
int main()
{
    std::cout << offsetof(A, e) << '\n'; // GCC和Clang上都是0,如果不加屬性這個值會是4或8
}

利用[[no_unique_address]],我們可以讓e和number共享內存空間,e不再占用1字節(jié)的額外內存,所以A只有一個long那么大。這是對于內存占用的影響。

第二個影響是對[[no_unique_address]]修飾的成員取地址和計算偏移量。被修飾的字段的地址和偏移量是不確定的。標準規(guī)定對于被修飾的成員,取地址和計算偏移量都是合法的,但沒規(guī)定取到的地址和偏移量具體應該是什么,只是說可能是其他類成員變量或者基類的地址。換個說法,標準的意思就是取地址是合法的,但得到的值是不確定的。這是一種ABI變更,不僅A的大小改變了,A的成員的內存布局也發(fā)生了很大的變化。

[[no_unique_address]]雖然讓被修飾字段的內存可以和其他對象重疊,但仍然需要遵守c++關于相同類型的不同對象需要有不同地址的規(guī)定:

struct Empty1 {};
struct Empty2 {};
struct A {
    long number;
    [[no_unique_address]] Empty1 e1;
    [[no_unique_address]] Empty2 e2;
};
struct B {
    long number;
    [[no_unique_address]] Empty1 e1;
    [[no_unique_address]] Empty1 e2;
};
static_assert(sizeof(A) == sizeof(long));
static_assert(sizeof(B) > sizeof(long));

注意B中我們的e1e2類型相同,為了不違反規(guī)則,e1e2中有一個是要有自己的獨立的內存空間的,另一個可以和其他類型的字重疊。至于那個字段有獨立空間哪個字段重疊,這個完全由編譯器決定。而類型不同,則兩個字段都可以和別的字段發(fā)生重疊,因此都不占額外的內存空間。

最后一點,如果類中只有一個非靜態(tài)數(shù)據(jù)成員,且這個成員有空類型,那么[[no_unique_address]]也不會生效:

struct Empty {};
struct A {
    [[no_unique_address]] Empty e;
};
struct B {
    Empty e;
};
static_assert(sizeof(A) == 1);
static_assert(sizeof(A) == sizeof(B));

屬性[[no_unique_address]]提供了一種比空基類優(yōu)化更簡單更清晰的方式讓空類型不再占用額外的內存。

no_unique_address的應用

如果你的代碼不是很在意ABI穩(wěn)定性的話,很多空基類優(yōu)化可以轉換成更簡單[[no_unique_address]]

我們還是拿前文中的libcxx的compressed_pair舉例子,轉換后的代碼如下:

struct compressed_pair {
    _LIBCPP_NO_UNIQUE_ADDRESS __attribute__((__aligned__(::std::__compressed_pair_alignment<T2>))) T1 Initializer1;
    // 內存對齊填充
    _LIBCPP_NO_UNIQUE_ADDRESS T2 Initializer2;
    // 內存對齊填充
};

_LIBCPP_NO_UNIQUE_ADDRESS是個宏,會被替換成[[no_unique_address]]或者[[msvc::no_unique_address]],因為號稱完全支持c++20的MSVC實際上沒有正確實現(xiàn)[[no_unique_address]]這個屬性,所以在MSVC上必須使用編譯器自己實現(xiàn)的效果類似的屬性,包裝代碼在llvm-project/libcxx/include/__config里:

#  if __has_cpp_attribute(msvc::no_unique_address)
// MSVC implements [[no_unique_address]] as a silent no-op currently.
// (If/when MSVC breaks its C++ ABI, it will be changed to work as intended.)
// However, MSVC implements [[msvc::no_unique_address]] which does what
// [[no_unique_address]] is supposed to do, in general.
#    define _LIBCPP_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]]
#  else
     // __no_unique_address__是clang和gcc實現(xiàn)的[[no_unique_address]]
#    define _LIBCPP_NO_UNIQUE_ADDRESS [[__no_unique_address__]]
#  endif

整體代碼要比利用空基類優(yōu)化的那版簡單很多。同時,這個實現(xiàn)也不會有奇怪的繼承關系了。

除此之外libcxx里還有很多類似的使用例,在不影響運行時效率的前提下大幅簡化了代碼。

總結

[[no_unique_address]]讓空類型的類數(shù)據(jù)成員有機會不再占用額外的內存空間,從而減輕了因為地址規(guī)定帶來的性能影響,同時還讓空基類優(yōu)化代碼得到了簡化的機會。

不過這個屬性會破壞ABI兼容性,所以重構的時候要慎重。然而它帶來的好處是很實在的,所以libcxx在去年用這個屬性重構了一大堆的代碼,并且在文檔里注明了哪些東西的ABI兼容被破壞了。對于開發(fā)者來說這是陣痛,但對于長期維護來說是利大于弊的。

關于這個屬性以及對于c++語言規(guī)范的影響,可以看這里:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0840r2.html

到此這篇關于C++20新增屬性[[no_unique_address]]詳解的文章就介紹到這了,更多相關C++20新增屬性[[no_unique_address]]詳解內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • C++ HLSL實現(xiàn)簡單的圖像處理功能

    C++ HLSL實現(xiàn)簡單的圖像處理功能

    本文主要介紹了HLSL實現(xiàn)簡單的圖像處理功能的方法,具有很好的參考價值,下面跟著小編一起來看下吧
    2017-02-02
  • Java?C++題解leetcode1598文件夾操作日志搜集器

    Java?C++題解leetcode1598文件夾操作日志搜集器

    這篇文章主要為大家介紹了Java?C++題解leetcode1598文件夾操作日志搜集器示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-09-09
  • C++深度探索虛函數(shù)指針示例

    C++深度探索虛函數(shù)指針示例

    虛函數(shù)主要通過V-Table虛函數(shù)表來實現(xiàn),該表主要包含一個類的虛函數(shù)的地址表,可解決繼承、覆蓋的問題,下面這篇文章主要給大家介紹了如何通過一篇文章帶你掌握C++虛函數(shù)的來龍去脈,需要的朋友可以參考下
    2022-12-12
  • C++實現(xiàn)LeetCode(91.解碼方法)

    C++實現(xiàn)LeetCode(91.解碼方法)

    這篇文章主要介紹了C++實現(xiàn)LeetCode(91.解碼方法),本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下
    2021-07-07
  • C語言二維數(shù)組運用實現(xiàn)掃雷游戲

    C語言二維數(shù)組運用實現(xiàn)掃雷游戲

    這篇文章主要為大家詳細介紹了C語言二維數(shù)組運用實現(xiàn)掃雷游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-06-06
  • C++全面覆蓋內存管理知識講解

    C++全面覆蓋內存管理知識講解

    本章主要介紹C語言與C++的內存管理,以C++的內存分布作為引入,介紹C++不同于C語言的內存管理方式(new delete對比 malloc free),感興趣的朋友來看看吧
    2022-06-06
  • c語言計算三角形面積代碼

    c語言計算三角形面積代碼

    c語言計算三角形面積,這只是一個小作業(yè),大家一起學習C語言吧,面積公式s = (a+b+c) / 2 area = sqrt(s * (s - a) * (s - b) * (s - c))
    2013-11-11
  • C語言16進制與ASCII字符相互轉換

    C語言16進制與ASCII字符相互轉換

    大家好,本篇文章主要講的是C語言16進制與ASCII字符相互轉換,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下
    2022-01-01
  • C++ vector使用以及底層核心剖析

    C++ vector使用以及底層核心剖析

    本文全面拆解了C++標準庫中的vector容器,從基礎使用、核心接口、迭代器失效到底層實現(xiàn),詳細介紹了vector的各個方面,感興趣的朋友跟隨小編一起看看吧
    2026-01-01
  • Qt+OpenCV利用幀差法實現(xiàn)車輛識別

    Qt+OpenCV利用幀差法實現(xiàn)車輛識別

    所謂幀差法也就是對連續(xù)圖像幀做差分運算,其結果與定義好的閾值比較,若大于閾值則為運動目標值為1,否則值為0?。本文將利用幀差法實現(xiàn)車輛識別,感興趣的可以了解一下
    2022-08-08

最新評論

宜兰县| 赣州市| 安图县| 通辽市| 修水县| 佛坪县| 古丈县| 建瓯市| 乐亭县| 扎鲁特旗| 莫力| 吴忠市| 年辖:市辖区| 习水县| 上饶市| 古田县| 峨眉山市| 冷水江市| 弥渡县| 桦南县| 合水县| 藁城市| 周口市| 景东| 天津市| 高唐县| 博湖县| 保山市| 册亨县| 南靖县| 万年县| 罗城| 康平县| 娱乐| 桐庐县| 肇东市| 容城县| 林周县| 容城县| 铜山县| 娱乐|