很多人一直都認為,類型擦除是一些高級語言(如Java)才具有的,其實在c++中也可以實現(xiàn)類型擦除。那么什么是類型擦除呢?我們都知道,C/c++是一門強類型語言,也就是說,編譯器必須知道數(shù)據(jù)是屬于什么類型的。說的直白一點,就是int類型還是其它什么類型,當然這些類型里也包括對象類型。而類型擦除,就是要把這些數(shù)據(jù)的類型抹去,或者說擦除掉,當然也可以理解為隱藏掉數(shù)據(jù)的類型。這不和剛剛說的強類型語言相反么,這樣做有什么用處呢?
做為強類型語言,所有的數(shù)據(jù)類型必須強調可知就會有一個顯示的問題,無法用一個通過的定義來描述這些數(shù)據(jù)類型非特定的行為。而往往是這些行為決定了設計上的解耦和分離。而這種行為的不同就是程序設計可擴展性和簡潔高效的前提和基礎,它可以顯著的降低程序中的侵入式設計。而在前面也反復提到過,c++從設計上本身是無法提供在運行時動態(tài)獲取數(shù)據(jù)對象的類型的功能的。這也讓類型擦除更具重要性。
這里需要提到一個定義:鴨子類型(英語:duck typing)是動態(tài)類型的一種風格。在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實現(xiàn)特定的接口,而是由“當前方法和屬性的集合”決定。這玩意兒有啥用呢,它可以不通過繼承實現(xiàn)多態(tài)。
那么數(shù)據(jù)類型有幾種方式呢,常見的有以下幾種:
1、多態(tài)
多態(tài)一般都很了解,可以通過父類型指針來管理子類型的指針,實現(xiàn)了對子對象類型的隱藏。但這種方式的缺點也非常明顯,需要寫繼承的類而且數(shù)據(jù)類型的隱藏并不全面,至少父類型始終要暴露出來。
2、模板
模板在前面也提到過多次,通過模板,可以實現(xiàn)對多種類型的行為操作進行抽象。比如一個模板的加法函數(shù)(比如多個類都調用此加法函數(shù)),可以實現(xiàn)對不同的數(shù)據(jù)類型的操作,如果對象也重載了加法,同樣也可以使用。不過,在基本類型中,這種模板函數(shù)抽象的行為就無能為力了。
3、容器
容器之所以可以擦除,其實更類似于模板,這個不用多說。另外還有一種組合容器,其實有些類似共用體。比如std::variant 。但此種方式其實也明確了數(shù)據(jù)的類型,只是多了幾種罷了,讓編譯器在真正使用時進行選擇。所以這種類型擦除更接近一種類型判斷。
4、通用類型
這個就得提到前面分析的std::any,通過它可以進行數(shù)據(jù)類型的動態(tài)轉換。但是其才應用時,仍然需要指定具體的類型后才能使用。
5、閉包
所謂閉包,其實就是Lambda表達式,通過它和std::function的配合,實現(xiàn)類型的擦除。
從上面的分析可以看到,其實具體實現(xiàn)上來看,如果想實現(xiàn)的比較理想(如any等的實現(xiàn)基本就是有限定范圍的),基本上都需要兩個基本條件,一個是帶約束的模板構造函數(shù),另外一個就是函數(shù)指針。再回頭和SBO系列相對比一下就可以明白為啥std::function的實現(xiàn)機制中的基本方式了。同時,也可以搞明白,其實類型擦除就是一種使用函數(shù)指針來實現(xiàn)多態(tài)的機制。
三、應用場景類型擦除的應用場景非常多,特別是在設計中,如何實現(xiàn)一個非繼承方式來搞定擴展而勿需考慮數(shù)據(jù)類型時,都可以使用這種方式。當然,如果看過MFC實現(xiàn)的源碼,也可以使用類似其侵入式的設計來實現(xiàn),但那個太復雜了,而且可擴展性也有限。業(yè)界現(xiàn)在基本已經(jīng)不在采用那種方式來設計程序。
另外上一篇中SBO系列中,其實也是采用了這種方式來實現(xiàn)。只要能夠明白函數(shù)指針的機制,這種設計就可以不拘泥于某種場景,自由的發(fā)揮。而所謂類型擦除不過是其中一個應用的重要實現(xiàn)而已。
這里的例程使用一個同學的例程,大家參考分析一下就明白了。
class MyFunction
{
private:
class FunctorWrapper
{
public:
virtual ~FunctorWrapper() = default;
virtual FunctorWrapper* clone() const = 0;
virtual void call() const = 0;
};
templateclass ConcreteWrapper : public FunctorWrapper
{
public:
ConcreteWrapper(const T& functor)
: functor(functor) { }
virtual ~ConcreteWrapper() override = default;
virtual ConcreteWrapper* clone() const
{
return new ConcreteWrapper(*this);
}
virtual void call() const override
{
functor();
}
private:
T functor;
};
public:
MyFunction() = default;
templateMyFunction(T&& functor)
: ptr(new ConcreteWrapper(functor)) { }
MyFunction(const MyFunction& other)
: ptr(other.ptr->clone()) { }
MyFunction& operator=(const MyFunction& other)
{
if (this != &other)
{
delete ptr;
ptr = other.ptr->clone();
}
return *this;
}
MyFunction(MyFunction&& other) noexcept
: ptr(std::exchange(other.ptr, nullptr)) { }
MyFunction& operator=(MyFunction&& other) noexcept
{
if (this != &other)
{
delete ptr;
ptr = std::exchange(other.ptr, nullptr);
}
return *this;
}
~MyFunction()
{
delete ptr;
}
void operator()() const
{
if (ptr)
ptr->call();
}
FunctorWrapper* ptr = nullptr;
};
再看一下不使用繼承的:
class MyFunction
{
private:
static constexpr std::size_t size = 16;
static_assert(size >= sizeof(void*), "");
struct Data
{
Data() = default;
char dont_use[size];
} data;
templatestatic void functorConstruct(Data& dst, T&& src)
{
using U = typename std::decay::type;
if (sizeof(U)<= size)
new ((U*)&dst) U(std::forward(src));
else
*(U**)&dst = new U(std::forward(src));
}
templatestatic void functorDestructor(Data& data)
{
using U = typename std::decay::type;
if (sizeof(U)<= size)
((U*)&data)->~U();
else
delete *(U**)&data;
}
templatestatic void functorCopyCtor(Data& dst, const Data& src)
{
using U = typename std::decay::type;
if (sizeof(U)<= size)
new ((U*)&dst) U(*(const U*)&src);
else
*(U**)&dst = new U(**(const U**)&src);
}
templatestatic void functorMoveCtor(Data& dst, Data& src)
{
using U = typename std::decay::type;
if (sizeof(U)<= size)
new ((U*)&dst) U(*(const U*)&src);
else
*(U**)&dst = std::exchange(*(U**)&src, nullptr);
}
templatestatic void functorInvoke(const Data& data)
{
using U = typename std::decay::type;
if (sizeof(U)<= size)
(*(U*)&data)();
else
(**(U**)&data)();
}
templatestatic void (*const vtables[4])();
void (*const* vtable)() = nullptr;
public:
MyFunction() = default;
templateMyFunction(T&& obj)
: vtable(vtables)
{
functorConstruct(data, std::forward(obj));
}
MyFunction(const MyFunction& other)
: vtable(other.vtable)
{
if (vtable)
((void (*)(Data&, const Data&))vtable[1])(this->data, other.data);
}
MyFunction& operator=(const MyFunction& other)
{
this->~MyFunction();
vtable = other.vtable;
new (this) MyFunction(other);
return *this;
}
MyFunction(MyFunction&& other) noexcept
: vtable(std::exchange(other.vtable, nullptr))
{
if (vtable)
((void (*)(Data&, Data&))vtable[2])(this->data, other.data);
}
MyFunction& operator=(MyFunction&& other) noexcept
{
this->~MyFunction();
new (this) MyFunction(std::move(other));
return *this;
}
~MyFunction()
{
if (vtable)
((void (*)(Data&))vtable[0])(data);
}
void operator()() const
{
if (vtable)
((void (*)(const Data&))vtable[3])(this->data);
}
};
templatevoid (*const MyFunction::vtables[4])() =
{
(void (*)())MyFunction::functorDestructor,
(void (*)())MyFunction::functorCopyCtor,
(void (*)())MyFunction::functorMoveCtor,
(void (*)())MyFunction::functorInvoke,
};
有一個可能約束的模板構造函數(shù)和函數(shù)指針。上面的封裝需要相關的模板類型實現(xiàn)了拷貝和仿函數(shù)的功能,否則不能使用。更多的可以參考一下原文:
https://www.cnblogs.com/jerry-fuyi/p/12664787.html#sbo
正所謂長江后浪推前浪,這位同學的博客質量相當高,大家可以看一看。
也可以參考:
https://quuxplusone.github.io/blog/2019/03/18/what-is-type-erasure/
https://zhuanlan.zhihu.com/p/351291649
https://zhuanlan.zhihu.com/p/351464404
https://zhuanlan.zhihu.com/p/374562057
一般來說,在一元類型操作中(換句話說就是函數(shù)中一般只有一個模板參數(shù))表現(xiàn)很良好,而有多個選項的話,則不太友好。
現(xiàn)在大家都很惶惑,不知道未來的方向??赡芴а弁?,都是不可預知的迷茫。越是在這個時候兒,越是要沉住氣,要不斷的錘煉自己的技術,趁著這個機會把基礎打得更牢更扎實;同時,要不斷的尋找方向,不要因為找不到方向就焦慮,因為可能下一次就會發(fā)現(xiàn)正確的方向。沒有方向要大膽去找,有了方向要大膽去試。
特別是年青人,現(xiàn)在屬于你們的世界。努力揮灑汗水,光明就在拐角。
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧