目錄:
trick:Hands-On Design Patterns With C++(零)前言zhuanlan.zhihu.com
本文提要:本文介紹兩個方面:(1)對公共接口的控制,即根據不同條件打開或關閉公共接口。(2)重新綁定政策,即如果更改主模闆類型,如何連相關的政策類型一并更新。
基于政策設計的進階技巧
使用政策控制公共接口(public interface)
前面我們已經使用過一種政策控制公共接口的方式。
我們通過從政策繼承來注入公共成員函數(參考第二篇
WithRelease
增加公共接口)。這種方法相當靈活且功能強大,
但是有兩個缺點:
- 一旦我們從政策類公有繼承,我們就無法控制注入的接口,政策的每個公共函數都是派生類就口的一部分!
- 如果要使接口生效,我們不得不将政策類強轉為派生類,它還必須有權通路所有資料成員及派生類繼承的其他政策。
現在,我們學習一種更直接的方法來操縱基于政策的類公共接口。 首先,讓我們區分兩個概念:
有條件地禁用現有成員函數
和
添加新成員函數
。
- 前者(有條件地禁用現有成員函數)是合理且安全的:如果特定實作不支援接口提供的操作,則就不應該提供此接口。
- 後者(添加新成員函數)是危險的:它使類公共接口的擴充失控。
C++有方法有選擇地啟用和禁用成員函數,我們經常通過
std::enable_if
實作,其基礎是我們在第7章講到的
SFINAE與重載解析管理
(Substitution Failure Is Not An Error 替代失敗不是錯誤)。
為了說明使用SFINAE實作政策可以有選擇地啟用/禁用成員函數,我們通過有選擇地禁用智能指針類中的
operator->()
來進行示範。
首先,讓我們回顧一下
std::enable_if
如何啟用或禁用特定成員函數:如果表達式
std::enable_if <value, type>
結果為true,則将編譯并産生指定的類型;如果值為false,則類型替換失敗(不生成任何類型結果)。 此模闆元函數的正确用法是在SFINAE上下文中,其中
類型替換失敗不會導緻編譯錯誤,而隻是禁用失敗的函數(更确切地說,它将其從重載解決方案集中删除) 。
由于使用
SFINAE
啟用或禁用成員函數所需的全部是編譯時常量,是以可以使用constexpr值定義其他政策:
struct WithArrow {
static constexpr bool have_arrow = true;
};
struct WithoutArrow {
static constexpr bool have_arrow = false;
};
現在,我們可以使用
std::enable_if
配合政策來控制是否産生
operator->()
公共接口,如果為true,傳回
T*
,否則類型推倒失敗:
// 使用允許->操作符政策
// 這種寫法如果将ArrowPolicy設定為WithoutArrow,即使不使用operator->(),也無法通過編譯!
template <typename T, typename DeletionPolicy = DeleteByOperator<T>, typename ArrowPolicy = WithArrow>
class SmartPtr : private DeletionPolicy {
public:
// 使用enable_if決定是否允許operate->(),如果為true傳回T*,注意,這樣寫如果将ArrowPolicy設定為WithoutArrow就不能通過編譯!下面會說明失敗原因。
std::enable_if_t<ArrowPolicy::have_arrow, T*> operator->() {
return p_;
}
.....
private:
T* p_;
};
遺憾的是,如果将ArrowPolicy設定為WithoutArrow,即使不使用
operator->()
,
也無法通過編譯! 失敗的原因是SFINAE
僅在模闆上下文中起作用,是以成員函數本身必須是模闆方法, 即使方法存在于類模闆中也是不夠的。将
operator->()
轉換為模闆成員函數很容易,但我們如何找出模闆參數類型呢?
operator->()
本身不帶任何參數,是以無法從參數中推導模闆類型!
幸運的是,還有一種方式 - 模闆參數可以具有預設值:
// C++14
template <typename T, typename DeletionPolicy = DeleteByOperator<T>, typename ArrowPolicy = WithArrow>
class SmartPtr : private DeletionPolicy
{
public:
...
// 将函數變為模闆函數,模闆類型U預設為T
template <typename U = T> std::enable_if_t<ArrowPolicy::have_arrow, U*> operator->() { return p_; }
template <typename U = T> std::enable_if_t<ArrowPolicy::have_arrow, const U*> operator->() const { return p_; }
private:
T* p_;
};
上述代碼基于C++14。 在C++11中,需要稍微冗長一些,因為沒有
std::enable_if_t
,兩者對比如下:
template <typename U = T> std::enable_if_t<ArrowPolicy::have_arrow, U*> operator->() { return p_; } // c++14
template <typename U = T> typename std::enable_if<ArrowPolicy::have_arrow, U*>::type operator->() { return p_; } // C++11
将
operator->()
變為模闆函數就可以使
SFINAE
生效,此時如果ArrowPolicy設定為WithoutArrow就會禁用
operator->()
但不會編譯失敗。
當模闆類型T并非一個類的時候,沒有
operator->()
操作(非類沒有成員變量)。是以,我們可以為所有類類型預設設定WithArrow政策,為所有其他類型預設設定WithoutArrow政策(同樣,使用C++14文法中的
std::conditional_t
即可):
// 使用std::conditional_t,當是類的情況下使用WithArrow,否則使用WithoutArrow
template <typename T, typename DeletionPolicy = DeleteByOperator<T>,
typename ArrowPolicy = std::conditional_t<std::is_class<T>::value, WithArrow, WithoutArrow>>
class SmartPtr : private DeletionPolicy
{
...
}
現在,我們也可以像啟用/禁用成員函數那樣有條件的啟用/禁用構造函數,唯一麻煩的一點是構造函數沒有傳回值,是以我們要将SFINAE測試隐藏在其它位置。我們将構造函數也模闆化,通過
std::enable_if
在模闆中添加一個未使用但有預設類型的額外參數。在推導預設類型時,替換就會有條件地失敗:
struct MoveForbidden {
static constexpr bool can_move = false;
};
struct MoveAllowed {
static constexpr bool can_move = true;
};
template <typename T, typename DeletionPolicy = DeleteByOperator<T>, typename MovePolicy = MoveForbidden>
class SmartPtr : private DeletionPolicy
{
public:
// 通過enable_if_t來執行SFINAE,如果政策允許move并且U是SmartPtr類型,啟用此構造函數。注意V是未使用的額外模闆類型。
template <typename U, typename V = std::enable_if_t<MovePolicy::can_move && std::is_same<U, SmartPtr>::value, U>>SmartPtr(U&& other)
: DeletionPolicy(std::move(other)), p_(other.p_) {
other.release();
}
.....
};
上述代碼的
std::is_same<U, SmartPtr>::value
在C++17中可以寫為
std::is_same_v <U, SmartPtr>
。
我們另一個例子是是否允許對象隐式轉換(目前我們的SmartPtr無法轉換為原生指針):
void f(C*);
SmartPtr<C> p(.....); // p是我們的SmartPtr
f((C*)(p)); // 精确轉換
f(p); // 隐式轉換
轉換操作符代碼如下:
template <typename T, .....>
class SmartPtr ..... {
public:
// 精确轉換與隐式轉換不能共存
explicit operator T*() { return p_; } // 精确轉換
operator T*() { return p_; } // 隐式轉換
.....
private:
T* p_;
};
上述代碼中explicit精确轉換版本與隐式轉換版本不能共存,我們還是使用政策生成他們,這次我們使用CRTP來實作。下面是一組政策示例,用于将智能指針轉換到原始指針:
template <typename P, typename T>
struct NoRaw { // 無指針轉換
};
template <typename P, typename T>
struct ExplicitRaw { // 精确比對指針轉換
explicit operator T*() { return static_cast<P*>(this)->p_; }
explicit operator const T*() const {
return static_cast<const P*>(this)->p_;
}
};
template <typename P, typename T>
struct ImplicitRaw { // 非精确比對指針轉換
operator T*() { return static_cast<P*>(this)->p_; }
operator const T*() const { return static_cast<const P*>(this)->p_; }
};
這些政策将所需的公共成員函數運算符添加到派生類。 由于需要使用派生類執行個體化的模闆,是以轉換政策是模闆模闆參數,其使用遵循
CRTP
:
template <typename T, .....,
template <typename, typename> class ConversionPolicy = ExplicitRaw> // 模闆模闆參數
class SmartPtr : ....., public ConversionPolicy<SmartPtr<T, ....., ConversionPolicy>, T>
{
public:
.....
private:
// 下面兩種方式設定ConversionPolicy都可以,但隻能保留一個
// friend class ConversionPolicy<SmartPtr<T, DeletionPolicy, CopyMovePolicy, ConversionPolicy>, T>;
template<typename, typename> friend class ConversionPolicy; // ConversionPolicy設定為友元
T* p_;
};
標明的轉換政策将其公共接口添加到派生類的公共接口。 一種政策添加了一組顯式轉換運算符,而另一種政策提供了隐式轉換。就像上面的CRTP示例中一樣,基類需要通路派生類的私有資料成員,是以要為整個模闆(及其每個執行個體)設定為友元:
friend class ConversionPolicy<SmartPtr<T, ....., ConversionPolicy>, T>; // 這是上一片代碼注釋掉的部分,兩種聲明友元的方法都可
重新綁定政策
正如我們所見,政策清單可能會很長。 如果我們隻想更改一長串政策清單中的一個政策,其他政策保持原樣,至少有兩種方法可以做到這一點。
第一種方法很常見,但是比較繁瑣:
在主模闆内部将模闆參數通過typedef或别名暴露出去。這是一個好習慣,如果沒有别名,則很難在編譯時找出模闆參數是什麼。例如,我們有一個
SmartPtr
,我們想知道什麼是删除政策。到目前為止,最簡單的方法是在
SmartPtr
類本身的幫助下:
template <typename T,
typename DeletionPolicy = DeleteByOperator<T>, // 正常模闆參數
typename CopyMovePolicy = NoMoveNoCopy, // 正常模闆參數
template <typename, typename> class ConversionPolicy = ExplicitRaw> // 模闆模闆參數
class SmartPtr : private DeletionPolicy,
public CopyMovePolicy,
public ConversionPolicy<SmartPtr<T, DeletionPolicy, CopyMovePolicy, ConversionPolicy>, T>
{
public:
// 使用别名
using value_t = T;
using deletion_policy_t = DeletionPolicy;
using copy_move_policy_t = CopyMovePolicy;
template <typename P, typename T1> using conversion_policy_t = ConversionPolicy<P, T1>; // 模闆模闆參數采用模闆别名,為别名聲明為模闆
.....
};
請注意,我們使用了兩種不同類型的别名:對于正常模闆參數(例如
DeletionPolicy
),我們可以使用typedef,或者使用其它别名聲明方法(這裡使用using)。但是,對于模闆模闆參數(例如
ConversionPolicy
),我們必須使用模闆别名(給别名聲明為模闆,例子中是
template <typename P, typename T1> using
)。
現在,如果我們需要使用某些相同的政策建立另一個智能指針,可以用一種簡單的方式獲得原始對象的政策:
SmartPtr<int, DeleteByOperator<int>, MoveNoCopy, ImplicitRaw> p_original(new int(42));
using ptr_t = decltype(p_original); // p_original的真實類型
SmartPtr<ptr_t::value_t, ptr_t::deletion_policy_t, ptr_t::copy_move_policy_t, ptr_t::conversion_policy_t> p_copy; // p_copy與p_original擁有相同類型的政策
// 隻改變SmartPtr的精度為double,其他政策保留原樣。這時就無法滿足要求,因為deletion_policy_t繼承主模闆類型,主模闆類型變成了double,而原始deletion_policy_t還是int類型
SmartPtr<double, ptr_t::deletion_policy_t, ptr_t::copy_move_policy_t, ptr_t::conversion_policy_t> q;
現在,
p_copy
和
p_original
具有完全相同的類型。 通過
decltype
獲得原始對象類型并得到其中的政策,這樣,我們就可以聲明出其他的
SmartPtr
,隻改變其中一小部分。但是此方法可能會産生一些問題,比如最後一行的q,隻改變了模闆類型(從int改為double),其他政策不變,但是其
deletion_policy_t
還是int類型而非double,這該怎麼辦呢?
如果政策與主模闆類型綁定,就需要通過第二種方法進行類型綁定,通過重新綁定類型模闆實作:
template <typename T>
struct DeleteByOperator {
void operator()(T* p) const {
delete p;
}
template <typename U> using rebind_type = DeleteByOperator<U>;
};
template <typename T,
typename DeletionPolicy = DeleteByOperator<T>,
typename CopyMovePolicy = NoMoveNoCopy,
template <typename, typename> class ConversionPolicy = ExplicitRaw>
class SmartPtr : private DeletionPolicy,
public CopyMovePolicy,
public ConversionPolicy<SmartPtr<T, DeletionPolicy, CopyMovePolicy, ConversionPolicy>, T>
{
public:
.....
// 重新綁定類型模闆,模闆類型U改變,DeletionPolicy删除政策模闆參數與rebind_type一緻
template <typename U> using rebind_type = SmartPtr<U,
typename DeletionPolicy::template rebind_type<U>, // 依賴主模闆類型!
CopyMovePolicy, // 不依賴主模闆類型
ConversionPolicy>; // 不依賴主模闆類型
};
在
rebind_type
重新綁定聲明中,
CopyMovePolicy
和
ConversionPolicy
兩個政策不依賴主模闆類型,而
DeletionPolicy
依賴主模闆類型,是以我們要對其重新綁定類型!這樣,我們就可以通過如下方式重新綁定類型建立
SmartPtr
:
SmartPtr<int, DeleteByOperator<int>, MoveNoCopy, ImplicitRaw> p(new int(42)); // int類型SmartPtr
using dptr_t = decltype(p)::rebind_type<double>; // 轉換double
dptr_t q(new double(4.2)); // double類型SmartPtr
如果我們可以直接通路智能指針類型,則可以将其用于重新綁定。 否則,我們可以使用
decltype()
來擷取原始類型。 指針q具有與p相同的政策,但指向double。同時,類型相關的政策(如
DeletionPolicy
)也會被更新。
我們已經讨論了實作和自定義政策的主要方法。下一篇,我們回顧一下我們學到的知識并對基于政策的設計的一般準則進行總結。