C++ valarray用法(物件初始化和成員函數)詳解

2020-07-16 10:04:29
定義在 valarray 標頭檔案中的 valarray 類別範本定義了儲存和運算元值序列的物件的型別,主要用來處理整數和浮點數,但也能夠用來儲存類型別的物件,只要類滿足一些條件:
  • 類不能是抽象的。
  • public 建構函式必須包含預設的建構函式和拷貝建構函式。
  • 解構函式必須是 public。
  • 類必須定義賦值運算子,而且必須是public。
  • 類不能過載 operator&()。
  • 成員函數不能拋出異常。
  • 不能儲存參照或 valarray 中用 const、volatile 修飾的物件。

如果類滿足所有這些約束,就可以使用它了。

valarray 模板為數值資料處理提供的功能比任何序列容器(例如 vector)都多。首先,最重要的是,它被設計為允許編譯器以一種不應用到序列容器的方式來優化它的操作效能。但是,編譯器是否優化並不依賴 valarray 操作的實現。

其次,有相當數量的一元和二元運算都是應用到 valarray 物件的內建型別上的。然後,有相當數量的內建一元函數可以將定義在 cmath 標頭檔案中的運算應用到每個元素上。最後,valarray 型別內建提供了將資料作為多維陣列使用的能力。

生成一個 valarray 物件很容易。下面是一些範例:
std::valarray<int> numbers (15); // 15 elements with default initial values 0
std::valarray<size_t> sizes {1, 2, 3}; // 3 elements with values 1 2 and 3
std::valarray<size_t> copy_sizes {sizes}; // 3 elements with values 1 2 and 3
std::valarray<double> values;  // Empty array
std::valarray<double> data(3.14, 10); // 10 elements with values 3.14
每個建構函式都生成了有給定元素數目的物件。在最後一條語句中,使用圓括號來定義 data 是必要的;如果使用花括號,data 會包含 3.14 和 10 兩個元素。也可以用從普通陣列得到的一定個數的值來初始化 valarray 物件。例如:
int vals[] {2, 4, 6, 8, 10, 12, 14};
std::valarray<int> valsl {vals, 5};   // 5 elements from vals: 2 4 6 8 10
std::valarray<int> vals2 {vals + 1, 4}; // 4 elements from vals: 4 6 8 10
後面會介紹其他的建構函式,因為它們有一些必須解釋的引數型別。

valarray物件的基本操作

valarray 物件和 array 容器一樣,不能新增或刪除元素。但是,能夠改變 valarray 容器中的元素個數,為它們賦新值。例如:
data.resize(50, 1.5); // 50 elements with value 1.5
在這個操作之前,如果 data 中儲存有元素,就會丟失它們的值。當需要得到元素的個數時,可以呼叫成員函數 size()。

成員函數 swap() 可以交換當前物件和作為引數傳入的另一個 valarray 物件的元素。例如:
std::valarray<size_t> sizes_3 {1, 2, 3};
std::valarray<size_t> sizes_4 {2, 3, 4, 5};
sizes_3.swap(sizes_4); // sizes_3 now has 4 elements and sizes_4 has 3
valarray 物件中包含的元素個數可以不同,但顯然兩個物件中的元素必須是相同型別的。成員函數 swap() 沒有返回值。非成員函數 swap() 函數模板做的事是一樣的,因此最後一條語句可以替換為:
std::swap(sizes_3,sizes_4); // Calls sizes_3.swap(sizes_4)
可以呼叫 valarray 的成員函數 min() 和 max() 來查詢元素的最小值和最大值。例如:
std::cout << "The elements are from " << sizes_4.min () << " to "<< sizes_4.max() << 'n';
為了能夠正常工作,元素必須是支援 operator<() 的型別。

成員函數 sum() 會返回元素的和,它是使用 += 運算子計算出來的。因此,可以在 valarray 中按如下方式計算元素的平均值:
std::cout << "The average of the elements " << sizes_4.sum()/sizes_4.size() << 'n';
這比使用 accumulate() 演算法要簡單得多。

valarray 沒有返回元素疊代器的成員函數,但有專門的非成員函數版本的 begin() 和 end() 可以返回隨機存取疊代器。這使我們能夠使用基於範圍的 for 迴圈來存取 valarray 中的元素,並且可以將演算法應用到元素上;後面你會看到一些範例。不能對 valarray 使用插入疊代器,因為沒有做這些事的成員函數,這也是它的大小不變的原因。

有兩個成員函數可以對元素進行移位(是對序列進行移位而不是移位單個元素值中的位元)。首先,成員函數 shift() 會將全部的元素序列移動由引數指定的位數。函數會返回一個新的 valarray 物件作為結果,保持原序列不變。如果引數是正數,元素會被左移;如果是負數,會右移元素。這看起來很像位移。

元素被移位之後,序列的左邊或右邊會為 0 或其他等同的型別。當然,如果不將移位元運算之後的結果存回原容器,原物件是不會改變的。下面是一些展示它如何工作的程式碼:
std::valarray<int> d1 {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto d2 = d1.shift(2);  // Shift left 2 positions
for(int n : d2) std::cout << n <<' ';
std::cout << 'n';//Result: 345678900
auto d3 = d1.shift(-3);//Shift right 3 positions
std::copy(std::begin(d3), std::end(d3),std::ostream_iterator<int>{std::cout, " "});
std::cout << std::endl;
// Result: 0 0 0 1 2 3 4 5 6
註釋解釋發生了什麼。為了展示可以使用的輸出方式,在兩個範例中用了不同的方式來輸出結果。valarray 模板為物件定義了賦值運算子,所以如果想替換原始序列,可以這樣寫:
d1 = d1.shift(2); // Shift d1 left 2 positions
元素移位的第二種方式是使用成員函數 cshift(),它會將元素序列迴圈移動由引數指定的數目的位置。序列會從左邊或右邊迴圈移動,這取決於引數是正數還是負數。這個成員函數也會返回一個新的物件。下面是一個範例:
std::valarray<int> d1 {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto d2 = d1.shift(2); // Result d2 contains: 3 4 5 6 7 8 9 1 2
auto d3 = d1.cshift(-3);// Result d3 contains: 7 8 9 1 2 3 4 5 6
apply() 函數是 valarray 的一個非常強大的成員函數,它可以將一個函數應用到每個元素上,並返回一個新的 valarray 物件為結果。valarray 類別範本中定義了兩個 apply() 函數模板:
valarray<T> apply(T func(T)) const;
valarray<T> apply(T func(const T&)) const;
有三件事需要注意。第一,所有版本都是 const,所以函數不能修改原始元素。第二,引數是一個有特定形式的函數,這個函數以 T 型別或 T 的 const 參照為引數,並返回 T 型別的值;如果 apply() 使用的引數不符合這些條件,將無法通過編譯。第三,返回值是 valarray<T> 型別,因此返回值總是一個和原序列有相同型別和大小的陣列。

下面是一個使用成員函數 apply() 的範例:
std::valarray<double> time {0.0,1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0}; // Seconds
auto distances = time.apply([](double t)
{
    const static double g {32.0}; // Acceleration due to gravity ft/sec/sec
    return 0.5*g*t*t;
}); // Result: 0 16 64 144 256 400 576 784 1024 1296
如果從高層建築向下丟磚塊,distances 物件可以計算出,過了相應的秒數後,磚塊下落了多少距離;為了使最後結果有效,建築的高度必須超過 1296 英尺。注意,不能使用從閉合區域捕獲的值作為引數的 lambda 表示式,因為這和函數模板中引數的規格不匹配。例如,下面的程式碼就無法通過編譯:
const double g {32.0};
auto distances = times.apply ([g] (double t) { return 0.5*g*t*t; }); // Won*t compile!
在 lambda 表示式中以值捕獲 g 會改變它的型別,以至於它不符合應用模板的規範。對於可以作為 apply() 引數的 lambda 表示式,捕獲語句必須為空。必須有一個和陣列型別相同的引數,並返回一個這種型別的值。

valarray 標頭檔案定義了許多來自於 cmath 標頭檔案的函數的過載版本,因此它們能夠應用到 valarray 物件的所有元素上。接受一個 valarray 物件為引數的函數有:

abs(), pow(), sqrt(), exp(), log(), log10(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), sinh(),cosh(), tanh()

下面是一個將這一節的程式碼段組合到一起的範例,它提供了一次將 cmath 函數用到 valarray 物件上的機會:
// Dropping bricks safely from a tall building using valarray objects
#include <numeric>                                       // For iota()
#include <iostream>                                      // For standard streams
#include <iomanip>                                       // For stream manipulators
#include <algorithm>                                     // For for_each()
#include <valarray>                                      // For valarray
const static double g {32.0};                            // Acceleration due to gravity ft/sec/sec

int main()
{
    double height {};                                      // Building height
    std::cout << "Enter the approximate height of the building in feet: ";
    std::cin >> height;

    // Calculate brick flight time in seconds
    double end_time {std::sqrt(2 * height / g)};
    size_t max_time {1 + static_cast<size_t>(end_time + 0.5)};

    std::valarray<double> times(max_time + 1);               // Array to accommodate times
    std::iota(std::begin(times), std::end(times), 0);        // Initialize: 0 to max_time
    *(std::end(times) - 1) = end_time;                       // Set the last time value

    // Calculate distances each second
    auto distances = times.apply([](double t) { return 0.5*g*t*t; });

    // Calculate speed each second
    auto v_fps = sqrt(distances.apply([](double d) { return 2 * g*d; }));

    // Lambda expression to output results
    auto print = [](double v) { std::cout << std::setw(5) << static_cast<int>(std::round(v)); };

    // Output the times - the last is a special case...
    std::cout << "Time (seconds): ";
    std::for_each(std::begin(times), std::end(times) - 1, print);
    std::cout << std::setw(5) << std::fixed << std::setprecision(2) << *(std::end(times) - 1);

    std::cout << "nDistances(feet):";
    std::for_each(std::begin(distances), std::end(distances), print);

    std::cout << "nVelocity(fps):  ";
    std::for_each(std::begin(v_fps), std::end(v_fps), print);

    // Get velocities in mph and output them
    auto v_mph = v_fps.apply([](double v) { return v * 60 / 88; });
    std::cout << "nVelocity(mph):  ";
    std::for_each(std::begin(v_mph), std::end(v_mph), print);
    std::cout << std::endl;
}
這樣就能夠確定從高層建築丟下磚塊時發生了什麼。下面是一些從迪拜塔得到的範例輸出,為了避免磚塊撞到牆壁,假設是從一根足夠長的桿子上扔下磚塊的:

Enter the approximate height of the building in feet: 2722
Time (seconds) : 0 1 2 3 4 5 6 7 8 9 10 11 12   13 13.04
Distances(feet): 0 16 64 144 256 400 576 784 1024 1296 1600 1936 2304   2704 2722
Velocity (fps) : 0 32 64 96 128 160 192 224 256 288 320 352 384 416 417
Velocity (mph) : 0 22 44 65 87 109 131 153 175 196 218 240 262  284 285

首先,如果我們真的這麼做了,肯定會坐牢,甚至更糟。其實,計算時忽略了阻力,但這本書是關於 STL 的,不考慮物理因素。最後,可以通過花費的時間乘以加速度得到速度,但那樣就不能將 sqrt() 應用到 valarray 上了,不是嗎?

所有的程式碼都很簡單。常數 g 被定義在全域性作用域內,因為這樣方便在程式碼的不同位置使用,包括 lambda 表示式。times 陣列中儲存的時間是以秒為單位的,用 iota() 演算法從 0 開始填充元素。最後的時間值,對應於磚塊撞到地面時,幾乎可以肯定不是整數,所以儲存的值是具體的。用 for_each() 來產生輸出,因為它比 copy() 更能控制輸出值和輸出流疊代器。最後的時間值不可能是整數秒,因此它被當作輸出中的特殊情況。lambda 表示式 print 是顯式定義的,因此它能被重用,從而輸出每組值。

為了得到或設定 valarray 中給定索引的元素,可以使用下標運算子 [],但下標運算子可以做的事不止於此,在本章的後面你就會看到。