本文只是將multi_array最基本的功能代碼做了一個扼要的分
析,正如文章開始所說,multi_array還有許多很有用的特性,如果讀者想充分了解multi_array的運作機制與實現技巧,就請深入完整地分
析multi_array的代碼吧,相信一定會大有收獲的!
動機
C++是一門自由的語言,允許你自由的表達自己的意圖,對不對? 所以我們既然可以new一個一維數組,也應該可以new出多維數組,對不對?先來看一個例子:
int* pOneDimArr = new int[10]; //新建一個10個元素的一維數組
pOneDimArr[0] = 0; //訪問
int** pTwoDimArr = new int[10][20]; //錯誤!
pTwoDimArr[0][0] = 0; //訪問
但是,很可惜,三四兩行代碼的行為并非如你所想象的那樣——雖然從語法上它們看起來是那么“自然”。
這里的問題在于,new int[10][20]返回的并非int**類型的指針,而是int (*)[20]類型的指針(這種指針被稱為行指針,對它“+1”相當于在數值上加上一行的大?。ū纠秊?/span>20),也就是說,讓它指向下一行),所以我們的代碼應該像這樣:
int (*pTwoDimArr)[20] = new int[i][20]; //正確
pTwoDimArr[1][2] = 0; //訪問
注意pTwoDimArr的類型——int(*)[20]是個很特殊的類型,它不能轉化為int**,雖然兩者索引元素的語法形式一樣,都是“p[i][j]”的形式,但是訪問內存的次數卻不一樣,語義也不一樣。
最關鍵的問題還是:以上面這種樸素的方式來創建多維數組,有一個最大的限制,就是:除了第一維,其它維的大小都必須是編譯期確定的。例如:
int (*pNdimArr)[N2][N3][N4] = new int[n1][N2][N3][N4];
這里N2,N3,N4必須都是編譯期常量,只有n1可以是變量,這個限制與多維數組的索引方式有關——無論多少維的數組都是線性存儲在內存中的,所以:
pTwoDimArr[i][j] = 0;
被編譯器生成的代碼類似于:
*( (int*)pTwoDimArr+i*20+j ) = 0;
20就是二維數組的行寬,問題在于,如果允許二維數組的行寬也是動態的,這里編譯器就無法生成代碼(20所在的地方應該放什么呢?)。基于這個原因,C++只允許多維數組的第一維是動態的。
不幸的是,正由于這個限制,C++中的多維數組就在大多數情況下變成了有名無實的無用之物。我們經??梢栽谡搲峡吹疥P于多維數組的問題,一般這類問題的核心都在于:如何模仿一個“完全動態的”多維數組。這里“完全動態”的意思是,所有維的大小都可以是動態的變量,而不僅是第一維。論壇上給出的答案不一而足,有的已經相當不錯,但是要么缺乏可擴展性(即擴展到N維的情況),要么在訪問元素的形式上遠遠脫離了內建的多維數組的訪問形式,要么消耗了額外的空間。歸根到底,我們需要的是一個類似這樣的多維數組實現:
//創建一個int型的3維數組,dim_sizes表示各維的大小:n1*n2*n3
multi_array<int,3> ma ( dim_sizes[n1][n2][n3] );
ma[i][j][k] = value; //為第i頁j行k列的元素賦值
ma[i][j] = value; //編譯錯!
ma[i] = value; //編譯錯!
ma[i][j][k][l] = value;//編譯錯!
這樣一個multi_array,能夠自動管理內存,擁有和內建多維數組一致的界面,并且各維的大小都可以是變量——正符合我們的要求??雌饋恚瑢崿F這個multi_array并非難事,但事實總是出乎想象,下面就是對boost中已有的一個multi_array實現的剖析——你幾乎肯定會發現一些出乎意料的(甚至是令人驚奇的)地方。
Boost中的多維數組實現——boost::multi_array
在Boost庫中就有一個用于描述多維數組的功能強大的MultiArray庫。它實現了一個通用、與標準庫的容器一致的接口,并且具有與C++中內建的多維數組一樣的界面和行為。正是這種設計,使得MultiArray庫與標準庫組件甚至用戶自定義的泛型組件之間可以具有很好的兼容性,使它們能夠很好協同工作。除此之外,MultiArray還提供了諸如改變大小、重塑(reshaping)以及對多維數組的視圖訪問等極為有用的特性,從而使MultiArray比其它描述多維數組的組件(譬如:std::vector< std::vector<…> > )更為便捷、高效。對示例程序進行調試、跟蹤是分析庫源代碼最有效的手段之一。我們就從MultiArray文檔中的示例程序入手:
// 略去頭文件包含
int main () {
// 創建一個尺寸為3×4×2的三維數組
#define DIMS 3 //數組是幾維的
typedef boost::multi_array<double,DIMS> array_type; // (1-1)
array_type A(boost::extents[3][4][2]); // (1-2)
// 為數組中元素賦值
A[1][2][0] = 120; // (1-3)
... ...
return 0;
}
在上述代碼中,(1-1)處的typedef是我們程序中使用的三維數組類型的聲明,很明顯,boost::multi_array的兩個模板參數分別代表數組元素的類型和數組的維度。而(1-2)處就是三維數組對象的構造語句。boost::extents[3][4][2]的意思是:定義一個3*4*2的三維數組。
下面我就為你層層剝開boost::extents的所有奧秘——
extents——與內建數組一致的方式
boost::extents是一個全局對象,在base.hpp中:
typedef detail::multi_array::extent_gen<0> extent_gen;
... ...
multi_array_types::extent_gen extents; //注意它的類型!
可見extents的類型為extent_gen,這個extend_gen則位于extent_gen.hpp中:
// extent_gen.hpp
template <std::size_t NumRanges>
class extent_gen {
range_list ranges_; // (2-1)
... ...
extent_gen(const extent_gen<NumRanges-1>& rhs, const range& a_range) // (2-2)
{
std::copy(rhs.ranges_.begin(),rhs.ranges_.end(),ranges_.begin());
*ranges_.rbegin() = a_range;
}
extent_gen<NumRanges+1>
operator[](index idx) // (2-3)
{ return extent_gen<NumRanges+1>(*this,range(0,idx)); }
};
所以,boost::extents[3][4][2]展開為操作符調用的方式就相當于:
extents.operator [](3).operator [](4).operator [](2);
extents是extent_gen<0>類型的,extents.operator [](3)應調用函數(2-3),此時NumRange為0,而返回類型是extent_gen<1>;再以該返回對象調用operator [](4),此時NumRange為1,而返回類型則是extent_gen<2>了。再看函數(2-3)的內容,其實就是將參數idx以range包裝一下再轉發給構造函數(2-2),注意此時調用的是extent_gen<NumRange+1>類型的構造函數。至于range(0,idx)則表示一個[0,idx)的區間。進入構造函數(2-2),我們注意到extent_gen<...>中具有public的成員ranges_,聲明位于(2-1)處,而ranges_就是一個容器,保存了一系列的range。
跟蹤這些代碼,基本了解了extents的工作方式:每調用一次operator [],都會返回一個extent_gen<NumRange+1>類型的對象,所以,對于boost::extents[3][4][2],依次返回的是:
extent_gen<1> => extent_gen<2> => extent_gen<3>
最后一個也是最終的返回類型——extent_gen<3>。其成員ranges_中,共有[0,3)、[0,4)、[0,2)三組區間。這三組區間指定了我們定義的multi_array對象的三個維度的下標區間,值得注意的是這些區間都是前閉后開的,即不包含上界值,這一點在后面的代碼中能夠看到。當boost::extents準備完畢后,就被傳入multi_array的構造函數,用于指定各維的下標區間:
// multi_array.hpp
explicit multi_array(const extent_gen<NumDims>& ranges) :
super_type((T*)initial_base_,ranges) {
allocate_space(); // (2-5)
}
這里,multi_array接受了ranges參數中的信息,取出其中各維的下標區間,然后保存,最后調用allocate_space()來分配底層內存。
使用extent_gen的好處
使用boost::extents作參數的構造過程和內建多維數組的方式一致,簡練直觀,語義清晰。首先,boost::extents使用“[]”,能讓人很容易想到內建多維數組的聲明,也很清晰地表達了每個方括號中數值的含義——表明各維度的容量區間;最關鍵的還是,使用boost::extents,可以防止用戶寫出錯誤的代碼,例如:
multi_array<int,3> A(boost::extents[3][4][2][5]);//錯!多了一維!
上面的語句是無法通過編譯,因為mult_array是個三維數組,而boost::extents后面卻跟了四個“[]”,這顯然是個錯誤;在語法層面,由于multi_array<int,3>的構造函數只能接受extent_gen<3>類型的參數,而根據我們前面對extents的分析,boost::extents[3][4][2][5]返回的卻是extent_gen<4>類型的對象,于是就會產生編譯錯誤。這種編譯期的強制措施阻止了用戶一不小心犯下的錯誤(如果你正在打瞌睡呢?),也很清晰明了地表達(強制)了語義的需求。
另一種替代方案及其缺點
另外,還有一種聲明各維大小的替代方式,就是使用所謂的Collection Concept,例如:
// 聲明一個shape(“形狀”),即各個維度的size
boost::array<int,3> shape = {{ 3, 4, 2 }};
array_type B(shape); //3*4*2的三維數組
這種方式將調用multi_array的第二種構造函數:
// multi_array.hpp
template <class ExtentList>
explicit multi_array( ExtentList const& extents ) :
super_type((T*)initial_base_,extents) {
boost::function_requires< // (2-4)
detail::multi_array::CollectionConcept<ExtentList> >();
allocate_space(); // (2-6)
}
這個構造函數的形參extents只要是符合collection concept就可以了——shape的類型為boost::array,當然符合這個concept。這個構造函數的行為與接受extents_gen的構造函數是一樣的——仍然是先取出各維的range保存下來,然后分配底層內存。至于(2-4)處的代碼,功能就是在編譯期檢查模板參數ExtentList是否符合Collection concept,實現細節在此不再贅述。
把這種方式與使用extent_gen的方式作一個簡單的比較,很容易就看出優劣:采用這種方式,就不能保證編譯期能夠進行正確性的檢查了,例如:
boost::array<int,4> shape = {{3,4,2,5}}; //一個四維數組的shape
multi_array<int,3> A(shape); // 竟然可以通過編譯!!
這里,用一個四維的shape來指定一個三維multi_array顯然是錯誤的,但是居然通過了編譯,這是由于這個構造函數將它的參數extents作為一個普通的collection來對待,構造函數根據自己的需求用iterator從extents中取出它所需要的數值——A是三維數組,于是構造函數從shape中取出前三個數值作為A三個維度的下標區間,而不管shape究竟是包含了幾個數值。這樣的語句在語義上是不清晰甚至錯誤的。但是既然這樣的構造函數存在,設計者自然有他的道理,文檔中就明確的表明,這個構造函數最大的用處就是編寫維度無關(dimension-independent)的代碼,除此之外multi_array庫默認為前一種構造函數。
multi_array的架構
無論采用哪一種構造函數,代碼流程卻是相似的——將一系列下標區間傳入基類的構造函數中去,基類構造完成之后就調用相同的allocate_space()函數(見(2-5)和(2-6)處),allocate_space,顧名思義,應該是為多維數組的元素分配空間的。但是對于這樣在派生類而非基類的構造中分配存儲空間的設計,可能的合理解釋就是:基類是個適配器(adapter),它決定了一切對原始數據的訪問規則,描述了multi_array對外界的接口。
順著基類的構造函數,我們繼續向multi_array的深處探索。
// multi_array_ref.hpp
template <typename T, std::size_t NumDims>
class multi_array_ref : //multi_array的基類??!
public const_multi_array_ref<T,NumDims,T*>
{
typedef const_multi_array_ref<T,NumDims,T*> super_type;
... ...
explicit multi_array_ref(T* base, //指向數組存儲空間的指針
const extent_gen<NumDims>& ranges): //下標區間
super_type(base,ranges) //把初始化的任務轉發給基類(3-1)
{ }
... ...
};
// multi_array_ref.hpp
class const_multi_array_ref : //multi_array_ref的基類!管理底層存儲!
public multi_array_impl_base<T,NumDims>
{
... ...
explicit const_multi_array_ref(TPtr base,
const extent_gen<NumDims>& ranges) :
base_(base), storage_(c_storage_order()) // (3-2)
{ init_from_extent_gen(ranges); }
... ...
storage_order_type storage_;//支持多種存儲策略?。?/span>3-3)
};
multi_array基類對象的構造之路途徑(3-1)處multi_array_ref的構造函數,延伸至(3-2)處const_multi_array_ref的構造函數——這里看似一個終結,因為再沒有參數傳遞給 const_multi_array_ref的基類multi_array_impl_base了。但是心中還是疑惑:為什么會有如此多層的繼承結構?這樣的類層次結構設計究竟有什么玄機呢?
多層繼承的奧秘——復用性
轉到基類const_multi_array_ref的聲明,似乎可以看出一些端倪:
template< ... >
class const_multi_array_ref {
... ...
//和所有的STL容器一致的迭代器界面?。?/span>
const_iterator begin() const;
const_iterator end() const;
... ...
//和std::vector一致的元素訪問界面??!
const_reference operator[](index i) const;
... ...
};
看到上面這些聲明,是不是有些面熟?STL!對,這些成員函數的聲明是與STL中container concept完全一致的。所謂與STL的兼容性正是在這里體現出來了。而const_multi_array_ref更是“類如其名”,const_multi_array_ref中所有訪問元素、查詢數組信息等成員函數都返回const的reference或iterator。而反觀multi_array_ref的聲明,其中只比const_multi_array_ref多了訪問元素、查詢數組信息的對應的non-const版本成員函數。那么const_multi_array_ref的基類multi_array_impl_base的職責是什么呢?接著展開類multi_array_impl_base的聲明,
multi_array_impl_base是屬于實現細節的,它的作用只是根據數組信息(const_multi_array_ref中的成員變量)計算偏移量、步長等,也就是把多維的下標最終轉化為一維偏移量。而multi_array_impl_base的基類——或者是value_accessor_n或者是value_accessor_one——的功能就是提供一個對原始數據的訪問。這在下文詳述。
至此,對multi_array的基類子對象大致有了了解——它們的繼承關系如下:
multi_array -> multi_array_ref -> const_multi_array_ref -> multi_array_impl_base -> value_accessor_n/value_accessor_one
其中每一層都擔任各自的角色:
¨ multi_array : 為數組元素分配空間,將各種操作轉發至基類。
¨ multi_array_ref : 提供與STL容器一致的數據訪問界面。也可以獨立出來作為一個adapter使用。
¨ const_multi_array_ref : 提供const的STL數據訪問界面。也可以作為一個const adapter使用。
¨ multi_array_impl_base及其基類 :最底層實現,提供一組對原始數據的基本操作。
這種架構看似復雜,卻提供了極高的復用性,其中的(const_)multi_array_ref都可以獨立出來作為一個adapter使用——例如:
int a[24]; //一維的10個元素數組
//把一維數組a看成一個3*4*2的三維數組:
multi_array_ref<int,3> arr_ref(a,boost::extents[3][4][2]);
arr_ref[i][j][k] = value; //和multi_array一樣的使用界面
倘若你不想讓multi_array來自動分配內存的話,你可以自行分配數組(可以位于棧上或堆上)然后用multi_array_ref把它包裝成一個多維的數組。
multi_array的存儲策略
接下來,就來看看multi_array的存儲策略,例如:C風格的多維數組存儲方式是按行存儲,而fortran恰恰相反,是按列存儲,甚至,用戶可能有自己的存儲策略要求。那么,如何支持多種風格的存儲策略呢?秘密就在于代碼(3-3)處,const_multi_array_ref的成員storage_——其類型為storage_order_type,下面的聲明指出了storage_order_type的“本來面目”——general_storage_order<NumDims>:
// multi_array_ref.hpp
... ...
typedef general_storage_order<NumDims> storage_order_type;
... ...
// storage_order.hpp
template <std::size_t NumDims>
class general_storage_order {
general_storage_order(const c_storage_order&){ //(4-1)
for (size_type i=0; i != NumDims; ++i)
{ ordering_[i] = NumDims - 1 - i; }
ascending_.assign(true);
}
... ...
boost::array<size_type,NumDims> ordering_;
boost::array<bool,NumDims> ascending_;
};
在(4-1)處的構造函數中,ordering_和ascending_是兩個數組,當函數(4-1)執行完畢后,ordering_中的元素應當是{NumDims-1, NumDims-2,...1,0},如果將這些元素作為各維度存儲順序的標識——具有較小ordering_值的維度先存儲——那么這和C語言中的存儲方式就完全一致了,ascending_勿庸置疑就是用來表明各維度是否升序存儲。其實general_storage_order還有一個模板構造函數,它是為了支持更為一般化的存儲策略(例如fortran的按列存儲或用戶自定義的存儲策略)。這里不作詳述。
除了存儲策略,const_multi_array_ref的構造還通過調用init_from_extent_gen函數,將extents中的內容取出來進行處理,并以此設定其它若干表述多維數組的變量((3-3)處其它一些變量),具體細節不再贅述。
現在關于一個多維數組的所有信息都已經準備齊備,可謂“萬事具備,只欠‘空間’”。multi_array下面要做的就是調用前面提到的allocate_space來為數組中的元素分配空間了。
// multi_array.hpp
void allocate_space() {
... ...
base_ = allocator_.allocate(this->num_elements(),no_hint);
... ... std::uninitialized_fill_n(base_,allocated_elements_,T());
}
原來在底層,存儲仍然是退化為一維數組的存儲,std::uninitialized_fill_n負責把該數組進行缺省初始化。allocate_space使用allocator_分配一塊連續空間用以存儲元素,其中num_elements()返回的就是數組各維度的大小的乘積,即數組的總元素個數。分配完之后,就將首指針賦給表述數組基地址的成員base_。至此multi_array的構造工作終于大功告成了。
一致性界面——GP的靈魂
multi_array的另一重要特性就是以支持與內建多維數組相同的訪問方式,即是說,multi_array支持以連續的operator[]來訪問數組元素。就以(1-3)處的賦值語句為例,讓我們看看multi_array是如何支持這種與內建數組兼容的訪問方式的。
// multi_array_ref.hpp
// 使用operator[]來訪問元素
reference operator[](index idx) {
return super_type::access(boost::type<reference>(),
idx,origin(),this->shape(),this->strides(),
this->index_bases());
}
這個調用轉入了value_accessor_n::access(...)之中:
// base.hpp
// in class value_accessor_n
template <typename Reference, typename TPtr>
Reference access(boost::type<Reference>,
index idx,TPtr base,const size_type* extents,
const index* strides,const index* index_base)
{
TPtr newbase = base + idx * strides[0];
return Reference(newbase,extents+1,
strides+1,index_base+1);
}
這個連續調用operator[]的過程和extend_gen是很類似的——每調用一層就返回一個“proxy”,然后在其上繼續調用operator[],如此往復...
那么如果以A[x1][x2][x3]方式訪問A中的元素,就相當于
A.operator[x1].operator[x2].operator[x3] //連續調用“[]”
這三次operator[]調用返回的類型依次為:
sub_array<T,2> -> sub_array<T,1> -> T&
注意,最后一次調用“[]”返回的恰好是對元素的引用(這就剛好證明了前面說的,只有“[]”的個數和數組的維數相同的時候,才能夠取出元素,否則你得到的要么sub_array<...>,要么會由于試圖在T&上繼續調用“[]”而編譯失?。┠敲?,這一切究竟是如何做到的呢?
sub_array的秘密
sub_array的定義在sub_array.hpp中:
// sub_array.hpp
template <typename T, std::size_t NumDims>
class sub_array : public const_sub_array<T,NumDims,T*>;
template <typename T, std::size_t NumDims, typename TPtr>
class const_sub_array :
public multi_array_impl_base<T,NumDims>;
//base.hpp
template <typename T, std::size_t NumDims>
class multi_array_impl_base:public
value_accessor_generator<T,mpl::size_t<NumDims> >::type ;
唔,原來sub_array最終繼承自multi_array_impl_base,后者的基類是value_accessor_generator中的一個typedef,會根據NumDims的不同而成為不同的類型:
// base.hpp
template <typename T, typename NumDims>
struct value_accessor_generator {
... ...
typedef typename //如果NumDims為1,則類型為value_accessor_one
mpl::apply_if_c<(dimensionality == 1),
choose_value_accessor_one<T>,
choose_value_accessor_n<T,dimensionality>
>::type type; //把這個類型作為multi_array_impl_base的基類!
};
很顯然,如果dimensionality == 1,那么“::type”就是value_accessor_one<T>。而只有對value_accessor_one使用“[]”才能返回T&,否則,“::type”被推導為:value_accessor_n,這只是個“proxy”而已,對它運用“[]”只會返回sub_array<T,NumDims-1>,從而繼續這個連續調用“[]”的過程。
取出元素
根據上面的分析,取元素的任務最終交給value_accessor_one,其成員函數access如下:
template <typename Reference, typename TPtr>
Reference access(boost::type<Reference>,index idx,TPtr base,
const size_type*,const index* strides,
const index*) const {
return *(base + idx * strides[0]); //終于取出了數據!
}
這里,access的返回類型Reference就是T&,即數組中元素類型的引用,從而可以將指定元素的引用返回,達到訪問數組元素的目的??吹竭@里,multi_array以內建數組訪問方式訪問數組元素的過程基本已經弄清楚了,至于其中一些細節,尤其是計算地址的細節,譬如:偏移量的計算、步長的使用等,皆已略去了。
現在也許你會有這樣的疑惑:以內建數組訪問方式訪問數組元素的能力真的如此重要嗎?費這么大力氣、寫這么多代碼還不如以多參數的方式重載operator[]呢!這么大代價真的值得嗎?值得!這是勿庸置疑的。以內建數組訪問方式訪問數組元素的能力最重要的表現就是,可以使使用者以與內建數組一致的方式對待multi_array。舉個例子:用戶編寫了一個函數模板,
template <typename ReturnType, typename _3DArray>
ReturnType func(_3Array& mda){//可以作用于內建多維數組
... ...
mda[x][y][z] = mda[z][x][y];
... ...
}
因為有了以內建數組訪問方式訪問數組元素的能力,這個func模板可以同時應用在內建數組和multi_array上(否則用戶就得為multi_array提供一個單獨的重載版本),如此一來,代碼的可重用性、可擴展性都大大提高了。
效率
效率是C++永恒的主題,MultiArray庫也不例外。執行時間效率上,縱觀MultiArray庫對數組元素的訪問代碼,雖然函數調用嵌套層數甚多,但多數調用都是簡單的轉發,在現在高度成熟的C++編譯器下,這些轉發的函數調用代碼應該可以很輕易地被優化掉,所以在效率上幾乎沒有什么損失。在空間效率方面,由于大量運用模板技術,基本能夠在編譯期決定的內容都已決定,沒有為運行期帶來不必要的空間上的負擔??偟目磥?,Boost.MultiArray庫的確是難得的高效又通用的多維數組的實現。
結語
本文只是將multi_array最基本的功能代碼做了一個扼要的分析,正如文章開始所說,multi_array還有許多很有用的特性,如果讀者想充分了解multi_array的運作機制與實現技巧,就請深入完整地分析multi_array的代碼吧,相信一定會大有收獲的!
目錄(展開《boost源碼剖析》系列文章)
本文來自:http://blog.csdn.net/pongba/archive/2007/04/11/1560738.aspx