本文章是由機器翻譯。

Windows 中的 C++

Visual c + + 2015年帶來現代 c + + 對遺留代碼

Kenny Kerr

Kenny Kerr使用 Windows 程式設計系統嚴重依賴于不透明的控制碼表示 C 樣式 Api 背後隱藏的物件。除非你程式設計在相當高的水準,很可能你會在業務的管理的各種類型的控制碼。一個控制碼的概念存在於許多庫和平臺,當然不是唯一到 Windows 作業系統的。早在 2011 年我第一次寫了智慧控制碼類範本 (msdn.microsoft.com/magazine/hh288076) 當 Visual c + + 開始引進一些初始的 C + + 11 的語言特徵。Visual c + + 2010年使它有可能寫方便和語義正確的控制碼的包裝,但它支援 C + + 11 為數極少,大量的精力被仍需正確書寫的一類。採用 Visual c + + 2015 年這一年,我以為我會重新回到這個主題,分享一些有關如何使用現代 c + + 來搞活一些老的 C 樣式庫的更多的想法。

最好的圖書館不分配任何資源,因此,需要最小包裝。我最喜歡的例子是 Windows 苗條櫥櫃讀寫器 (SRW) 鎖。這裡是所有它需要創建和初始化是準備使用 SRW 鎖:

SRWLOCK lock = {};

SRW 鎖結構包含只是單一的 void * 指標,還有什麼能幫你整理 !它必須初始化的之前使用,唯一的限制是不能移動或複製。顯然,任何現代化具有更多地與異常安全性而持有的鎖,而不是資源管理。儘管如此,現代 c + + 可以説明確保這些簡單的要求得到滿足。首先,我可以使用能力來初始化非靜態資料成員他們聲明位置準備使用 SRW 鎖:

class Lock
{
  SRWLOCK m_lock = {};
};

負責初始化,但鎖仍可複製和移動。為此我需要刪除預設的複製建構函式和複製指派運算子:

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
};

這可以防止拷貝和移動。聲明類的公共部分在這些傾向于產生更好的編譯器錯誤訊息。當然,現在我需要提供預設建構函式,因為你不再假設:

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock() noexcept = default;
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
};

儘管我沒有寫任何代碼本身 — — 編譯器生成的一切對我來說 — — 我現在可以創建一個鎖很簡單:

Lock lock;

編譯器將禁止任何企圖複製或移動鎖:

Lock lock2 = lock; // Error: no copy!
Lock lock3 = std::move(lock); // Error: no move!

當然,可以那麼簡單地添加獲取和釋放該鎖以各種方式方法。SRW 鎖,如其名稱所示,提供共用的讀者及專屬作家鎖定語義。圖 1 提供最小集方法的簡單的獨佔鎖定。

圖 1 簡單高效 SRW 鎖

class Lock
{
  SRWLOCK m_lock = {};
public:
  Lock() noexcept = default;
  Lock(Lock const &) = delete;
  Lock & operator=(Lock const &) = delete;
  void Enter() noexcept
  {
    AcquireSRWLockExclusive(&m_lock);
  }
  void Exit() noexcept
  {
    ReleaseSRWLockExclusive(&m_lock);
  }
};

查閱"進化的同步 Windows 和 c + + 中"(msdn.microsoft.com/magazine/jj721588) 對魔術這個令人難以置信的小鎖定背後的原始的詳細資訊。剩下的是提供一點異常安全性周圍鎖擁有權。當然不想寫成下面這樣:

lock.Enter();
// Protected code
lock.Exit();

相反,我想就一個鎖衛兵去照顧的獲取和釋放鎖為某個給定的範圍:

Lock lock;
{
  LockGuard guard(lock);
  // Protected code
}

這種鎖的警衛可以簡單地守住參考下­說謊鎖:

class LockGuard
{
  Lock & m_lock;
};

像鎖類本身,它最好警衛班不允許複製或移動,要麼是:

class LockGuard
{
  Lock & m_lock;
public:
  LockGuard(LockGuard const &) = delete;
  LockGuard & operator=(LockGuard const &) = delete;
};

剩下的工作是為一個建構函式來輸入鎖和析構函數退出該鎖。圖 2 裹這一例子。

圖 2 簡單的鎖警衛隊

class LockGuard
{
  Lock & m_lock;
public:
  LockGuard(LockGuard const &) = delete;
  LockGuard & operator=(LockGuard const &) = delete;
  explicit LockGuard(Lock & lock) noexcept :
    m_lock(lock)
  {
    m_lock.Enter();
  }
  ~LockGuard() noexcept
  {
    m_lock.Exit();
  }
};

為了公平起見,Windows SRW 鎖是相當獨特的小寶石,大多數圖書館將需要存儲位或某種必須顯式地管理的資源。我已經展示了如何最好地管理 COM 介面指標在"COM 智慧指標重新"(msdn.microsoft.com/magazine/dn904668),所以現在我將重點介紹的更一般情況及不透明控制碼。正如我寫震區­ously,控制碼類範本必須提供方式進行參數化不僅類型的控制碼,也在該控制碼已關閉,方式,甚至什麼確切地表示無效控制碼。並不是所有圖書館都使用 null 或零值表示無效控制碼。我原來的控制碼類範本假定調用方將提供給必要的語義和類型資訊控制碼性狀類。在這幾年很多、 很多性狀類寫完,我開始意識到其中絕大多數遵循類似的模式。而且,任何 c + + 開發人員會告訴你,模式是什麼樣的範本都是擅長描述。所以,和控制碼類範本,我現在使用控制碼性狀類範本。控制碼性狀類範本並不是必需的但確實簡化大多數定義。下面是它的定義:

template <typename T>
struct HandleTraits
{
  using Type = T;
  static Type Invalid() noexcept
  {
    return nullptr;
  }
  // Static void Close(Type value) noexcept;
};

請注意 HandleTraits 類範本的提供和它特別不提供了什麼。我寫了這麼多不正確方法返回 nullptr 值,這似乎是顯而易見的預設。另一方面,每個具體特徵類必須提供其自身密切的方法,原因很明顯。注釋的生命僅僅是作為有規律可循。類型別名同樣是可選的只是為了定義自己的特性類方便來自此範本。因此,我可以定義為 Windows CreateFile 函數,返回的檔案控制代碼的特徵類,如下所示:

struct FileTraits
{
  static HANDLE Invalid() noexcept
  {
    return INVALID_HANDLE_VALUE;
  }
  static void Close(HANDLE value) noexcept
  {
    VERIFY(CloseHandle(value));
  }
};

CreateFile 函數返回的 INVALID_HANDLE_VALUE 值,如果該功能失敗。否則,必須使用 CloseHandle 函數關閉結果控制碼。這是非常不尋常。Windows CreateThreadpoolWork 函數將返回一個 PTP_WORK 控制碼表示的工作物件。這是只是一個不透明的指標,nullptr 價值自然返回失敗。其結果是,為工作物件的特徵類可以利用 HandleTraits 類範本中,可以我節省一點的鍵入:

struct ThreadPoolWorkTraits : HandleTraits<PTP_WORK>
{
  static void Close(Type value) noexcept
  {
    CloseThreadpoolWork(value);
  }
};

所以什麼實際控制碼類範本樣子?好吧,它可以簡單地依賴于給定的特徵類、 推斷該控制碼的類型和調用 Close 方法,根據需要。推理形式 decltype 運算式,以確定該控制碼的類型:

template <typename Traits>
class Handle
{
  using Type = decltype(Traits::Invalid());
  Type m_value;
};

這種做法使特徵類的作者不必包含類型別名或 typedef 提供類型顯式和冗余。關閉控制碼是第一次訂購的業務和安全密切的説明器方法,塞進私人部分的控制碼類範本:

void Close() noexcept
{
  if (*this) // operator bool
  {
    Traits::Close(m_value);
  }
}

這種密切的方法依賴于顯式的布林運算子,以確定是否該控制碼需要調用特性類來實際執行該操作之前關閉。公共顯式的布林運算子比我控制碼類範本從 2011 年的另一項改進,,它可以簡單地實現為顯式轉換運算子:

explicit operator bool() const noexcept
{
  return m_value != Traits::Invalid();
}

這解決了各種問題,比傳統的方法,同時避免可怕的隱式轉換,編譯器將否則允許實現布林運算子定義當然很容易。所做的另一語言改進利用在這篇文章,是顯式刪除特殊成員的能力,我會做,現在複製建構函式和複製指派運算子:

Handle(Handle const &) = delete;
Handle & operator=(Handle const &) = delete;

預設建構函式可以依賴特徵類初始化該控制碼可預測的方式:

explicit Handle(Type value = Traits::Invalid()) noexcept :
  m_value(value)
{}

析構函數可以簡單地依靠親密助手:

~Handle() noexcept
{
  Close();
}

所以,副本不允許,但除了 SRW 鎖,不能想起那一控制碼資源,不允許在記憶體中移動其控制碼。移動控點的能力是極大地方便了。可能指一點,分離和附加,或也許分離和重置的兩個個別行動涉及到移動控點。分離涉及釋放的控制碼的調用方的擁有權:

Type Detach() noexcept
{
  Type value = m_value;
  m_value = Traits::Invalid();
  return value;
}

向調用方返回的控制碼值和控制碼物件副本無效以確保其析構函數不調用 Close 方法性狀類提供的。補充附加或重置操作包括關閉任何現有的控制碼,然後假定新的控制碼值的擁有權:

bool Reset(Type value = Traits::Invalid()) noexcept
{
  Close();
  m_value = value;
  return static_cast<bool>(*this);
}

重置方法預設為該控制碼的值無效,並成為一種簡單的方式來過早地關閉控制碼。它還作為一種方便返回顯式的布林運算子的結果。我發現自己經常寫下面的模式:

work.Reset(CreateThreadpoolWork( ... ));
if (work)
{
  // Work object created successfully
}

在這裡,我能依靠明確的布林運算子以事後檢查有效性的控制碼。能夠把這壓縮成單個運算式可以很方便:

if (work.Reset(CreateThreadpoolWork( ... )))
{
  // Work object created successfully
}

現在,我在我可以很簡單,執行移動操作移動建構函式開頭的地方有這種握手方式:

Handle(Handle && other) noexcept :
  m_value(other.Detach())
{}

Rvalue 引用上調用該分離方法和新建的控制碼有效次搶斷從另一個控制碼物件的擁有權。移動賦值操作符只是稍微更複雜:

Handle & operator=(Handle && other) noexcept
{
  if (this != &other)
  {
    Reset(other.Detach());
  }
  return *this;
}

第一次執行身份檢查以避免附加一個封閉的控制碼。底層的 Reset 方法不會執行這種檢查,因為這會涉及兩個額外的分支,每次移動作業。其中一個是審慎的。兩個是多餘的。當然,移動語義是隆重,但交換語義是甚至更好,尤其是如果你要將控制碼存儲在標準容器:

void Swap(Handle<Traits> & other) noexcept
{
  Type temp = m_value;
  m_value = other.m_value;
  other.m_value = temp;
}

自然,非成員和小寫交換功能是泛型的必要條件:

template <typename Traits>
void swap(Handle<Traits> & left, Handle<Traits> & right) noexcept
{
  left.Swap(right);
}

控制碼類範本的最後潤色進來的一雙的 Get 和 Set 的方法的形式。Get 是明顯的:

Type Get() const noexcept
{
  return m_value;
}

它只是返回底層的控制碼值,可能需要將傳遞給不同的庫函數。集也許是較為明顯:

Type * Set() noexcept
{
  ASSERT(!*this);
  return &m_value;
}

這是一個間接的設置的操作。這一說法凸顯了這一事實。過去,我已經叫此 GetAddressOf,但該名稱掩蓋或違背其真正的目的。在地方圖書館返回一個控制碼作為 out 參數的情況下需要進行這種間接的設置的操作。WindowsCreateString 函數是從很多人只是一個示例:

HSTRING string = nullptr;
HRESULT hr = WindowsCreateString( ... , &string);

我可以用這種方式調用 WindowsCreateString,然後將結果控制碼附加到一個控制碼的物件,或者我可以簡單地使用 Set 的方法直接承擔擁有權:

Handle<StringTraits> string;
HRESULT hr = WindowsCreateString( ... , string.Set());

這是更為可靠,明確指出在其中的資料流程動的方向。控制碼類範本還提供了常用的比較運算子,但由於顯式轉換運算子的語言的支援,這些都不再是避免隱式轉換的必要條件。他們只是派上用場,但我會為你去探索。控制碼類範本是從現代 c + + 的 Windows 運行時只是另一個例子 (moderncpp.com)。


Kenny Kerr 是一個位於加拿大,以及作者 Pluralsight 和微軟最有價值球員的電腦程式員。他的博客 kennykerr.ca ,你可以跟著他在 Twitter 上 twitter.com/kennykerr