跳轉到內容

更多 C++ 習語/臨時基類

來自華夏公益教科書,開放的書本,構建開放的世界

臨時基類

[編輯 | 編輯原始碼]

降低建立臨時物件的成本。

臨時物件通常在 C++ 程式執行期間建立。C++ 運算子(一元、二元、邏輯等)的結果和按值返回的函式始終會產生臨時物件。對於內建型別,建立臨時物件的成本很低,因為編譯器通常使用 CPU 暫存器來操作它們。但是,對於在建構函式中分配記憶體並可能在複製建構函式中需要昂貴的複製操作的使用者定義類,建立臨時物件可能會非常昂貴。臨時物件通常是浪費的,因為它們的壽命通常很短,並且只存在於被分配給命名物件(左值)的情況下。即使它們的壽命很短,C++ 語言規則也要求編譯器建立和銷燬臨時物件以保持程式的正確性。(實際上,RVO 和 NRVO 最佳化允許消除一些臨時物件)。建立和銷燬臨時物件的成本會對使用重型物件的程式的效能產生負面影響。

例如,考慮使用堆記憶體儲存整數陣列的 Matrix 類。該類使用常用的 RAII 習語來管理資源:在建構函式中分配,在解構函式中釋放。複製建構函式和複製賦值運算子負責維護對分配記憶體的獨佔所有權。

void do_addition(int * dest, const int * src1, const int * src2, size_t dim)
{
  for(size_t i = 0; i < dim * dim; ++i, ++dest, ++src1, ++src2)
    *dest = *src1 + *src2;
}

class Matrix
{
  size_t dim;
  int * data;

public:

  explicit Matrix(size_t d)
    : dim(d), data(new int[dim*dim]())
  {
    for(size_t i = 0;i < dim * dim; ++i)
      data[i] = i*i;  
  }
  
  Matrix(const Matrix & m)
    : dim(m.dim), data(new int[dim*dim]())
  {
    std::copy(m.data, m.data + (dim*dim), data);
  }
  
  Matrix & operator = (const Matrix & m)
  {
    Matrix(m).swap(*this);
    return *this;
  }
  
  void swap(Matrix & m)
  {
    std::swap(dim, m.dim);
    std::swap(data, m.data);
  }
  
  Matrix operator + (const Matrix & m) const
  {
    Matrix result(this->dim);
    do_addition(result.data, this->data, m.data, dim);
    return result;
  }

  ~Matrix()
  {
    delete [] data;
  }
};

在表示式中使用此類物件,例如以下表達式,會存在多個性能問題。

Matrix m1(3), m2(3), m3(3), m4(3);
Matrix result = m1 + m2 + m3 + m4;

每次求和都會建立臨時 Matrix 物件,並且立即銷燬。對於 (((m1 + m2) + m3) + m4) 中的每對括號,都需要一個臨時物件。建立和銷燬每個臨時物件都需要記憶體分配和釋放,這非常浪費。

臨時基類習語是一種減少臨時物件在算術表示式(如上述表示式)中的開銷的方法。但是,這種習語也有主要的缺點,如文末所述。

解決方案和示例程式碼

[編輯 | 編輯原始碼]

臨時基類習語(TBCI)不會改變臨時物件的建立事實,但會降低(大幅降低)建立它們的成本。這是透過識別建立臨時物件的位置,並使用一個建立和銷燬都輕量級的型別來實現的。與 C++11 不同,C++03 沒有語言支援的方式來區分臨時物件(右值)和命名物件(左值)。常引用是 C++03 中可用於將臨時物件繫結到引用的唯一方法。

在 TBCI 習語中,每個重量級物件的類都由兩個類表示。一個類 D(代表派生類)用於表示命名物件,而另一個類 B(代表基類)用於表示臨時物件。使用者預計只在變數/函式宣告中使用 D 類,因為 D 型別的物件的行為與常規物件相同。例如,在複製 D 物件時會執行深度複製。

B 類用於在算術表示式中建立的中間臨時物件。只要所有由 D 物件計算的結果都分配給另一個 D 物件,B 型別物件的建立和銷燬對使用者來說是透明的。B 類和 D 類之間的關鍵區別在於複製行為。每當從 D 物件建立 B 物件或 D 物件時,都會執行深度複製(即新的記憶體分配和資料複製)。另一方面,每當從 B 物件建立 B 物件或 D 物件時,都會執行淺層複製(即只複製指標)。這些規則也適用於賦值運算子,區別在於可能需要在左側物件中刪除現有記憶體(例如,從 B 到 B 或 D 的賦值)。此外,與 D 物件不同,B 物件不擁有資料的獨佔所有權,因此在呼叫解構函式時不會銷燬資料。

構造和賦值
深度複製 淺層複製
從 D 到 B、D 從 B 到 B、D

B 類和 D 類的介面經過精心設計,以完全支援彼此之間的轉換,並尊重上述記憶體管理規則。以下示例是上面顯示的 Matrix 類的 TBCI 版本。Matrix 類是主類,而 TMatrix 類用於表示臨時矩陣。

class Matrix;
class TMatrix 
{
  size_t dim;
  int * data;
  bool freeable;
  void real_destroy();

 public:

  explicit TMatrix(size_t d);
  TMatrix(const TMatrix & tm);
  TMatrix(const Matrix & m);
  TMatrix & operator = (const Matrix & m);
  TMatrix & operator = (const TMatrix & tm);
  TMatrix & operator + (const Matrix & m);
  TMatrix & operator + (TMatrix tm);
  ~TMatrix();
  void swap(TMatrix &) throw();

  friend class Matrix;
};

class Matrix : public TMatrix 
{
public:
  explicit Matrix(size_t dim);
  Matrix(const Matrix & tm);
  Matrix(const TMatrix & tm);
  Matrix & operator = (const Matrix & m);
  Matrix & operator = (const TMatrix & tm);
  TMatrix operator + (const Matrix & m) const;
  TMatrix operator + (const TMatrix & tm) const;
  ~Matrix();
};

上面兩個類的介面揭示了幾件事。不僅類翻了一番,成員函式的數量也(幾乎)翻了一番。特別是,為 MatrixTMatrix 都聲明瞭複製建構函式、複製賦值運算子和過載的 operator +。這是必要的,以確保在 MatrixTMatrix 物件相遇的所有可能情況下,行為都是明確定義的。

TMatrix 物件的唯一建立方式是透過 Matrix 類中的過載 operator +。每當兩個 Matrix 類相加時,結果都會作為 TMatrix 物件返回。任何後續 Matrix 物件的加法結果都將儲存在作為第一次加法結果的同一個 TMatrix 物件中。這消除了為臨時矩陣分配和釋放記憶體的需要。例如,TBCI 透過以下方式執行 4 個矩陣的加法來實現效率。

Matrix result = (((m1 + m2) + m3) + m4);
...
Matrix result = (((T1) + m3) + m4);
...
Matrix result = ((T1) + m4);
...
Matrix result = (T1);

只有在第一次建立 TMatrix 物件時才分配新記憶體。加法的結果儲存在顯示為 T1 的臨時 TMatrix 物件中。最後,結果必須分配給 Matrix 物件以確保記憶體資源不會洩漏。

請注意,算術運算的其他組合也是可能的。特別是,當使用其他優先順序更高的運算子(如二元乘法和除法)時,TMatrix 物件可能以不同的順序建立。為了簡單起見,示例中只顯示了二元加法,並使用括號來強制優先順序。例如,考慮以下執行順序。

((m1 + m2) + (m3 + m4))
...
((T1) + (T2))
...
(T1)

為了獲得上述行為,兩個類(MatrixTMatrix)都以習語的方式實現,如下所示。

/***** Implementation *****/
void do_addition(int * dest, const int * src1, const int * src2, size_t dim)
{
  for(size_t i = 0; i < dim * dim; ++i, ++dest, ++src1, ++src2)
    *dest = *src1 + *src2;
}

void do_self_addition(int * dest, const int * src, size_t dim)
{
  for(size_t i = 0; i < dim * dim; ++i, ++dest, ++src)
    *dest += *src;
}

void populate(int *data, size_t dim)
{
  for(size_t i = 0;i < dim * dim; ++i)
    data[i] = i*i;
}

TMatrix::TMatrix(size_t d) 
: dim(d), data (new int[dim*dim]()), freeable(0)
{
  populate(data, dim);
}

TMatrix::TMatrix(const TMatrix & tm)
: dim(tm.dim), data(tm.data), freeable(0)
{}

TMatrix::TMatrix(const Matrix & m)
: dim(m.dim), data(new int[dim*dim]), freeable(0)
{
  std::copy(data, data + dim*dim, m.data);
}

TMatrix & TMatrix::operator = (const Matrix & m)
{
  std::copy(m.data, m.data + (m.dim * m.dim), data);
  return *this;
}

TMatrix & TMatrix::operator = (const TMatrix & tm)
{
  real_destroy();
  dim = tm.dim;
  data = tm.data;
  freeable = 0;
  return *this;
}

TMatrix & TMatrix::operator + (const Matrix & m)
{
  do_self_addition(this->data, m.data, dim);
  return *this;
}

TMatrix & TMatrix::operator + (TMatrix tm)
{
  do_self_addition(this->data, tm.data, dim);
  tm.real_destroy();
  return *this;
}

TMatrix::~TMatrix() 
{
  if(freeable) real_destroy();
}

void TMatrix::swap(TMatrix & tm) throw()
{
  std::swap(dim, tm.dim);
  std::swap(data, tm.data);
  std::swap(freeable, tm.freeable);
}

void TMatrix::real_destroy()
{
  delete [] data;
}

Matrix::Matrix(size_t dim) 
: TMatrix(dim) 
{}

Matrix::Matrix(const TMatrix & tm)
: TMatrix(tm)
{}

Matrix::Matrix(const Matrix & m)
: TMatrix(m)
{}

Matrix & Matrix::operator = (const Matrix &m)
{
  Matrix temp(m);
  temp.swap(*this);
  return *this;
}

Matrix & Matrix::operator = (const TMatrix & tm)
{
  real_destroy();
  dim = tm.dim;
  data = tm.data;
  freeable = 0;
  return *this;
}

TMatrix Matrix::operator + (const Matrix & m) const
{
  TMatrix temp_result(this->dim);
  do_addition(temp_result.data, this->data, m.data, dim);
  return temp_result;
}

TMatrix Matrix::operator + (const TMatrix & tm) const 
{
  TMatrix temp_result(tm);
  do_addition(temp_result.data, this->data, tm.data, dim);
  return temp_result;
}

Matrix::~Matrix() 
{
  freeable = 1;
}

以下是上面程式碼的一些亮點。TMatrix 物件只作為 Matrix 加法的結果建立。兩個 Matrix 物件相加會生成一個新分配的 TMatrix 物件。另一方面,當兩個被加物件中的任何一個都是 TMatrix 型別時,結果將儲存在臨時物件引用的記憶體中。

TMatrix 建構函式分配記憶體,但只有在 freeable 為 true 時才銷燬它。Matrix 解構函式負責釋放記憶體。與 Matrix 的複製建構函式不同,TMatrix 的複製建構函式執行淺層複製。但是,如前所述,從 Matrix 構造 TMatrix 會執行深度複製,並擁有其獨佔記憶體。

TMatrix::operator + 是這種習語中的關鍵函式。請注意,加法的結果儲存在臨時物件本身(do_self_addition)中,從而避免了另一次分配。接受 TMatrix 物件作為引數的 TMatrix::operator + 很有趣,因為它按值接受 TMatrix 物件。這是必要的,因為兩個臨時物件的加法應該只生成一個臨時物件,另一個物件必須銷燬。TMatrix 物件確實管理它們的記憶體,因此,其中一個物件是使用 TMatrix::real_destroy 顯式銷燬的。如果右側 TMatrix 繫結到常引用,這將是不可能的。

Matrix 類是根據 TMatrix 實現的。從另一個 Matrix 構造 Matrix 會進行分配和複製。另一方面,從 TMatrix 構造 Matrix 只會複製指標,因為 TMatrix 不會刪除資料。相同的規則適用於 Matrix 類的賦值運算子。最後,Matrix 解構函式只是將 freeable 標誌更改為 true,從而導致刪除記憶體。為了正確處理一些很少發生的案例,也處理了對 TMatrix 物件的賦值。從 Matrix 的賦值只是複製操作,而從 TMatrix 類的賦值需要銷燬其中一個並對指標進行淺層複製。

注意事項

這種習語有一些嚴重的缺點。

  • 計算結果必須分配給派生類物件(Matrix 物件)。否則,程式中肯定會出現資源洩漏。這種約定通常很難維護。
  • 因此,這種習語甚至不是基本的異常安全的。如果任何中間記憶體分配操作丟擲異常,很可能就會發生資源洩漏。

已知用法

[編輯 | 編輯原始碼]

TBCI 庫

[編輯 | 編輯原始碼]

參考文獻

[編輯 | 編輯原始碼]
華夏公益教科書