簡體   English   中英

移動后如何處理無效狀態,特別是對於具有驗證構造函數的對象?

[英]How to handle invalid state after move especially for objects with validating constructor?

我為函數的參數創建了一個類來委派其驗證以及函數重載的目的。

從構造函數中拋出可以保證對象要么以有效狀態構造,要么根本不構造。 因此,不需要引入任何檢查成員函數,如explicit operator bool() const

// just for exposition
const auto &certString = open_cert();
add_certificate(cert_pem{certString.cbegin(), certString.cend()}); // this will either throw 
                                                                    // or add a valid certificate.
                                                                    // cert_pem is a temporary

但是,有些問題我看不到一個吸引人的解決方案: 參數驗證類本身可能是非持久的 - 僅用於作為臨時對象進行驗證。 但是允許持久化的類呢? 那是在函數調用之后生活:

// just for exposition
const auto &certString = open_cert();

cert_pem cert{certString.cbegin(), certString.cend()}; // allowed to throw
cert_pem moved = std::move(cert); // cert invalidated
cert_pem cert_invalid = std::move(cert); // is not allowed to throw

add_certificate(cert_invalid); // we lost the whole purpoce 

我可以看到幾種在不引入狀態檢查(從而聲明類有狀態)函數的情況下處理此問題的方法:

  1. 移動后聲明對象“不可用”。 - 一個非常簡單的災難配方
  2. 聲明刪除了移動構造函數和賦值運算符。 僅允許復制- 復制資源可能非常昂貴。 如果使用 PIMPL 成語,甚至不可能。
  3. 當需要持久化對象時使用堆分配- 這看起來最明顯。 但是對性能有不必要的懲罰。 特別是當某個類有幾個這樣的對象作為成員時 - 在構造時會有幾個內存分配。

這是2)的代碼示例:

/**
     * Class that contains PEM certificate byte array.
     * To be used as an argument. Ensures that input certificate is valid, otherwise throws on construction.
     */
    class cert_pem final
    {
    public:

        template <typename IterT>
        cert_pem(IterT begin, IterT end)
            : value_(begin, end)
        {
            validate(value_);
        }

        const std::vector<uint8_t>& Value() const noexcept(false)
        {
            return value_;
        }

        cert_pem (const cert_pem &) = default;
        cert_pem & operator=(const cert_pem &) = default;

        cert_pem (cert_pem &&) = delete;
        cert_pem & operator=(cert_pem &&) = delete;

    private:
        /**
         * \throws std::invalid_argument
         */
        static void Validate(const std::vector<uint8_t>& value) noexcept(false);
        static void ValidateNotEmpty(const std::vector<uint8_t>& value) noexcept(false);

    private:
        std::vector<uint8_t> value_;
    };

有沒有其他方法可以解決這個問題而沒有這些缺點? 還是我必須選擇上述之一?


我認為使用參數驗證類的一個好方法是不允許它持久化——只允許臨時對象。 但我不確定在 C++ 中是否可行。

您試圖一次維護兩個不變量,並且它們的語義存在沖突。 第一個不變量是證書的有效性。 第二個是內存管理。

對於第一個不變量,您決定不能有無效的構造對象,但對於第二個不變量,您決定對象可以是有效的或未指定的 這只是可能的,因為釋放在某處有檢查。

沒有辦法解決這個問題:您要么為第一個添加檢查,要么解耦不變量。 一種解耦它們的方法是遵循std::lock_guard的設計

cert c = open_cert(); // c is guaranteed to not have memory leaks and is movable
{
    cert_guard cg{c};  // cg is guaranteed to be valid, but cg is non-movable
}

但是等等,您可能會問,如何將有效性轉移到另一個cert_guard

好吧,你不能。

這就是您為第一個不變量選擇的語義:它在對象的生命周期內完全有效。 這就是重點

† 就證書而言,未指定且無效。

該問題旨在設計一種類型:

  1. 類型的對象總是滿足給定的不變量
  2. 該類型的對象作為非臨時對象“可用”

然后問題從 (2)跳躍到要求類型是可移動的。 但這不一定是:復制和移動操作可以定義為刪除。 這個問題無法激發為什么移動操作是必要的。 如果這是一個需要,它來自一個未說明的要求。 不可移動的類可以放置在地圖中,從函數返回,並以許多其他方式使用。 誠然,使用它可能會更痛苦,但它可以使用。

所以這是未列出的一個選項:將復制和移動操作定義為已刪除。

否則,假設我們確實想要:

  1. 類型的對象總是滿足給定的不變量
  2. 類型是可移動的

並不沖突。 每個可復制的類都是可移動的,在這里復制是一種有效的策略。 請記住,移動操作通過允許對源進行變異來允許“可能更智能”的副本。 仍然有兩個 C++ 對象,它仍然是一個邏輯副本,但假設在當前狀態下不再需要源(因此您可以從中竊取!)。 C++ 接口沒有區別,只是在移動操作后類型的完全未經檢查的記錄行為。

將移動操作定義為已刪除為您提供了一個可復制的類。 這是您列出的第二個選項。 從 xvalue ( cert_pem moved = std::move(cert) ) 分配仍將編譯,但不會使源無效。 它仍然會被語言認為是可移動的。 正如您所注意到的,折衷方案可能很昂貴。 請注意,PIMPL 作者可以為他們的類型提供復制操作,這是他們對類型接口應該是什么做出的選擇,並且習語不會阻止它。

第三個選擇是第二個的版本。 通過將值放在shared_ptr后面,可以使復制成本高的類型變得便宜。 但是我們仍然依靠復制作為移動的策略。

第一種選擇相當於削弱(1)中的不變量。 在 C++ 中,移動對象滿足與普通對象不同的一組不變量是非常典型的。 這很煩人,但在許多情況下,這是我們能做的最好的事情。 當只能存在一個滿足不變量的對象時(想想:非空unique_ptr ),移出的對象必須違反它。

接受的答案相當於我的第一個選項與延遲構造相結合:將復制和移動操作定義為已刪除。 如果對象被移出,則創建守衛可能會拋出。 守衛只是保持不變的類型,它是不可移動的。 我們可以推遲它的建設,因為這樣的類型很難管理。 我們通過保留一個足夠了解如何構造它的對象來做到這一點。 這種策略以其他形式存在( emplace函數和piecewise_construct構造函數以在其最終位置構造對象,工廠函數以隨意構造對象等)。

但是,在我看來,已接受答案中的描述還有一些不足之處。 希望在可移動的同時保持不變性(這是假設的)。 可移動並不要求被移動的對象滿足不變量或未指定。 這是該類型的作者做出的選擇,根據我的閱讀,可以選擇的正是問題所在。 盡管給出的示例僅涉及內存,並且第一個答案提到了內存,但我對這個問題的理解更為籠統:在可移動類中維護不變量。

知道所有可復制的類都是可移動的,移動是一個“智能”副本,並且在移動操作中和移動操作之后有兩個對象將有助於理解為什么這里有如此有限的選項。 必須讓源對象處於某種狀態。

我的建議是擁抱放射性移動物體。 這就是標准庫中的方法,默認的移動操作會經常遵守。 對於此類類型,移出對象必須有一些“空”狀態,因此所有類型實際上都是可選的,並且還可以定義默認構造函數來獲取處於該空狀態的對象。

暫無
暫無

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

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