我认为这种问题通常意味着抽象不好。
在这个例子中,一个高效的矩阵将拥有一个单一连续的数组,其中matrix(i,j)
被转换为幕后的array[i*n_columns+j]
。在许多情况下,i,j
接口更简单(否则你只需使用向量),但没有理由限制用户直接访问底层数组元素 - 更不用说在你自己的矩阵类内部了!
另一种说法是,你正在使用创建(i,j)
抽象的类,现在你想要取消工作的另一层抽象。这会耗费CPU时间和你的时间,并且会产生混乱的代码。相反,确保你(和你的用户)可以通过直接元素访问和迭代器访问底层数组:
public:
auto & operator [] (size_t i)
{
return data[i];
}
const auto & operator [] (size_t i) const
{
return data[i];
}
Matrix& operator += (const Matrix& other)
{
for (size_t i = 0; i < size(); ++i)
data[i] += other[i];
return *this;
}
Matrix& operator -= (const Matrix& other)
{
for (size_t i = 0; i < size(); ++i)
data[i] -= other[i];
return *this;
}
你可能会认为这样会破坏封装性,但实际上并不会。用户可以访问并编辑元素,但无法更改矩阵大小或访问原始指针。不过,为了回答你关于避免循环的更一般的问题,我将假设我们只能访问迭代器:
Matrix& operator += (const Matrix& other)
Matrix& operator -= (const Matrix& other)
好了,这样做比较好,但我们可以将任何迭代器或容器的重复部分抽象出来,像这样:
template <class Func, class ... Its>
void ForArrays (size_t size, Func&& f, Its && ... its)
{
for (size_t i = 0; i < size; ++i)
f(its[i]...);
}
template <class Func, class ... Cs>
void ForContainers (Func&& f, Cs && ... cs)
{
size_t size = (cs.size(), ...);
assert(((size == cs.size()) && ...));
ForArrays(size, f, cs.begin()...);
}
演示
现在我们可以重写操作符:
Matrix& operator += (const Matrix& other)
{
ForContainers([](auto& lhs, auto rhs){lhs += rhs}, *this, other);
return *this;
}
Matrix& operator -= (const Matrix& other)
{
ForContainers([](auto& lhs, auto rhs){lhs -= rhs}, *this, other);
return *this;
}
我认为这里的教训是永远不要撤消抽象化的工作;如果它对你的代码的某个部分没有用处,那就完全避免使用它(在那部分代码中)。取消一层抽象化需要另外一层抽象化的支持,这就是为什么有些人会放弃面向对象编程的原因。