更多 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();
};
上面两个类的接口揭示了几件事。不仅类翻了一番,成员函数的数量也(几乎)翻了一番。特别是,为 Matrix
和 TMatrix
都声明了复制构造函数、复制赋值运算符和重载的 operator +
。这是必要的,以确保在 Matrix
和 TMatrix
对象相遇的所有可能情况下,行为都是明确定义的。
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)
为了获得上述行为,两个类(Matrix
和 TMatrix
)都以习语的方式实现,如下所示。
/***** 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
对象)。否则,程序中肯定会出现资源泄漏。这种约定通常很难维护。 - 因此,这种习语甚至不是基本的异常安全的。如果任何中间内存分配操作抛出异常,很可能就会发生资源泄漏。
- K. Spanderen 和 Y. Xylander: Gut kalkuliert, “Effiziente Numerik mit C++”, iX 166–173 页,1996 年 11 月
- 等离子体模拟数值类文档,Kurt Garloff