簡體   English   中英

當涉及分配器時,是否有類似於復制和交換習語的東西?

[英]Is there something similar to the copy-and-swap idiom when allocators are involved?

關於復制和交換成語有幾個很好的答案,例如,解釋復制和交換成語解釋移動語義 適用於復制和移動分配的基本習慣用法如下所示:

T& T::operator=(T other) {
    this->swap(other);
    return *this;
}

此分配適用於復制和移動分配,因為other分配是復制或移動構造,具體取決於分配的右側是左值還是右值。

現在讓有狀態的分配器進入畫面:如果T在分配器類型上參數化,例如std::vector<S, A> ,上述成語並不總是有效,具體來說, std::allocator_traits<A>包含指示是否應該傳播分配器的三種類型:

  1. std::allocator_traits<A>::propagate_on_container_copy_assignment
  2. std::allocator_traits<A>::propagate_on_container_move_assignment
  3. std::allocator_traits<A>::propagate_on_container_swap

這三個特征的默認值是std::false_type (參見 20.6.8.1 [allocator.traits.types] 第 7、8 和 9 段)。 如果這些特征中的任何一個是std::false_type並且分配器是有狀態的並且可能比較不相等,則正常的復制和交換習語不起作用。 對於復制分配,修復相當簡單:

T& T::operator= (T const& other) {
    T(other, this->get_allocator()).same_allocator_swap(*this);
    return *this;
}

That is, first the object is copied supplying the allocator object of the LHS and then the members are swapped using a function which works if both objects use the same allocator, ie, when other.get_allocator() == this->get_allocator() .

移動分配時,如果可以移動,最好不要復制 RHS。 如果分配器相同,則可以移動 RHS。 否則 object 需要使用適當的分配器復制,從而導致像這樣的賦值運算符

T& T::operator= (T&& other) {
    T(std::move(other), this->get_allocator()).same_allocator_swap(*this);
    return *this;
}

這里的方法是在傳遞分配器的同時移動構造一個臨時的。 這樣做假定類型T確實有一個“移動構造函數”,它為 object state 和一個分配器提供一個T&&來指定要使用的分配器。 根據分配器的不同或相同,將負擔放在移動構造函數上進行復制或移動。

由於第一個參數的傳遞方式不同,因此不能將復制和移動賦值折疊到賦值運算符的一個版本中。 因此,他們需要將 arguments 作為參考,並且需要明確復制或移動 arguments 以抑制復制省略的可能性。

當涉及分配器時,是否有更好的方法來處理賦值運算符?

你似乎暗示着經典的復制/交換成語為所有的情況下工作propagate_on_特質是不假。 我不相信這種情況。 例如考慮:

std::allocator_traits<A>::propagate_on_container_copy_assignment::value == true
std::allocator_traits<A>::propagate_on_container_swap::value == false

經典的復制/交換習語分配運算符不會將分配器從rhs傳播到lhs,而是在兩個分配器狀態不相等的情況下進入未定義行為的狀態。

您對復制賦值運算符的重寫也不適用於此propagate_on_ traits的組合,因為它從不在復制賦值上傳播分配器。

如果想要遵循std :: containers的規則,我不相信復制/交換習慣用法。

我為自己保留了一個“分配器行為”備忘單,描述了這些成員應該如何表現(英語而不是標准eze)。

復制賦值運算符

如果propagate_on_container_copy_assignment::value為true,則copy分配分配器。 在這種情況下,如果在賦值之前分配器不相等,則首先釋放lhs上的所有內存。 然后繼續復制分配值,而不轉移任何內存所有權。 this->assign(rhs.begin(), rhs.end())

移動賦值運算符

  1. 如果propagate_on_container_move_assignment::value為true,則釋放lhs上的所有內存,移動分配分配器,然后將內存所有權從rhs轉移到lhs。

  2. 如果propagate_on_container_move_assignment::value為false,並且兩個分配器相等,則釋放lhs上的所有內存,然后將內存所有權從rhs轉移到lhs。

  3. 如果propagate_on_container_move_assignment::value為false,並且兩個分配器不相等,則移動assign,如同this->assign(make_move_iterator(rhs.begin()), make_move_iterator(rhs.end())

這些描述旨在實現最高性能,同時遵守容器和分配器的C ++ 11規則。 只要有可能,內存資源(例如vector capacity() )要么從rhs傳輸,要么在lhs上重用。

復制/交換習慣用法總是丟棄lhs上的內存資源(例如vector capacity() ),而是在lhs上釋放它們之前搶先分配這些資源。

為了完整性:

交換

如果propagate_on_container_swap::value為true,則交換分配器。 無論如何,交換內存所有權。 如果propagate_on_container_swap::value為false且分配器不相等,則行為未定義。

特例:PMR 感知容器

在過去的幾個月里,我一直在處理 pmr 容器,我真的開始喜歡它們了。 因此,以@Howard Hinnant 的方式,我為最有效的 pmr 感知容器制作了自己的小樣板備忘單,希望有人會覺得這很有幫助,因為有很多事情需要考慮(我什至沒有知道這是否就是全部……)。

正如我們所知,我們只會處理硬編碼的 std::byte 分配器,我們可以做出某些假設,最值得注意的是我們從不傳播分配器(DLS 移動除外,見下文)。 這使我們免於檢查:這是我當前的實現:

演示

#include <memory_resource>

struct pmr_container
{
    using allocator_type = std::pmr::polymorphic_allocator<std::byte>;

    allocator_type get_allocator() const {
        return subcontainer_.get_allocator(); // 1) retrieve
    }

    explicit pmr_container(allocator_type allocator = {}) // 2) global allocator default
        :   subcontainer_( allocator )  // 3 parenthesis init
    { }

    pmr_container(const pmr_container& other, allocator_type allocator = {}) // 4 const ref
        :   subcontainer_( allocator )
    {
        operator=(other);
    }

    pmr_container(const pmr_container&& other) noexcept // 5) noexcept propagating move (DLS)
        :   subcontainer_( other.get_allocator() )
    {
        operator=(std::move(other));
    }

    pmr_container(const pmr_container&& other, allocator_type allocator) // 6) pmr aware move
        :   subcontainer_( allocator ) // 7 may be the same
    {
        operator=(std::move(other));
    }

    pmr_container& operator=(const pmr_container& other) {
        scalar_ = other.scalar_;
        subcontainer_ = other.subcontainer_;
        return *this;
    }

    pmr_container& operator=(pmr_container&& other) {
        // 8 relay check to data member
        scalar_ = std::move(other.scalar_); // Just for the looks
        subcontainer_ = std::move(other.subcontainer_);
        return *this;
    }

    // void* self_managed_ = nullptr;
    int scalar_ = 10;
    std::pmr::vector<std::pmr::string> subcontainer_;
};

int main()
{
    pmr_container a;
    pmr_container b = a;
    pmr_container c = std::move(b);
}
  1. 當我們將分配器信息推送到數據成員中時,我們不會浪費自己存儲它們的存儲空間
  2. 默認初始化會產生全局分配器(allocator = {})
  3. 將分配器傳播到成員構造函數。 警告:不要使用花括號 + 使用顯式來防止從分配器意外構造容器以適應例如向量 initializer_list 構造函數
  4. 確保使用 const 引用復制 ctor 以便能夠綁定到 rvalue-refs(只是一般提示
  5. 使用“骯臟的小秘密”(DLS) ( David Sankel ) 移動構造函數來允許 nothrow 移動分配(否則可能會選擇復制 ctor)。 該 ctor 始終使用其他 memory 資源。
  6. 用於 pmr 容器的常規移動 ctor(容器將傳播它們的分配器,例如在 emplace_back()-calls 中)
  7. 雖然分配器可能不同,但這並不能保證。 如果分配器相同,我們實際上可以進行“移動”。 否則無論如何我們都必須復制所有內容。
  8. 在這個簡單的例子中,我們甚至不需要檢查分配器是否相同。 如果我們調用正確的賦值運算符,這將為我們在托管資源(例如向量)中完成。

補充說明:

  1. 自我管理的資源 如果我們實際上自己進行資源管理(因為std::pmr::unique_ptr尚不存在:/),我們實際上必須檢查分配器的分配器相等性。 我沒有對此進行任何思考,但它不應該太復雜(如果有人需要,我可能會更新它)。

  2. 其他談話:@Pablo Halpers 談話分配器:好的部分提到了這里提到的某些概念。 但是,存在某些低效率和可能的誤解,可能會導致錯誤(根據經驗)。 1) 最值得注意的是,他根據 swap() 實現了賦值運算符。 如果您使用 std::swap 這將導致循環依賴,因為 swap 將調用移動 ctor 本身,在這個和他的示例中是基於賦值運算符實現的。 2) 他使用 if &other == 這個檢查,這在執行的操作方面是多余的,只是增加了額外的分支開銷。

  3. 復制和交換:您不能使用復制和交換習語,因為默認容器不允許您這樣做。 引用cppreference

polymorphic_allocator 不會在容器復制分配、移動分配或交換上傳播。 結果,使用 polymorphic_allocator 的容器的移動分配可能會拋出,並且交換兩個使用 polymorphic_allocator 的容器,其分配器不比較相等會導致未定義的行為。

這基本上是說,即使您將為您的容器實現自己的朋友交換 function 重載,您也不能使用 std::swap 及其重載來交換 pmr-aware 數據成員而不去 UB-land。 除此之外,根據容器大小ref ,交換習語可以使代碼慢 70%。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM