有没有一种方法可以提取多个类似函数的外部循环?

6

例子:我希望从这些操作函数中提取嵌套的for循环,这些函数除了一行代码外都是相同的。

// Add two matrices
Matrix& operator+=(const Matrix& other)
{
    for (int i = 0; i < this->m_rows; i++)
    {
        for (int j = 0; j < this->m_cols; j++)
        {
            (*this)(i, j) = (*this)(i, j) + other(i, j); // Only difference
        }
    }
    return *this;
}

// Subtract two matrices
Matrix& operator-=(const Matrix& other)
{   
    for (int i = 0; i < this->m_rows; i++)
    {
        for (int j = 0; j < this->m_cols; j++)
        {
            (*this)(i, j) = (*this)(i, j) - other(i, j); // Only different
        }
    }
    return *this;
}

3
使用一个内部私有成员函数,该函数期望一个 lambda 作为参数? - Sebastian
这将是一种访问者模式。 - Sebastian
2个回答

10
你可以编写一个函数模板,接受一个二元函数,并将其应用于循环内部的所有元素对。
template<typename Op>
void loops(const Matrix& other, Op op)
{
    for (int i = 0; i < this->m_rows; i++)
    {
        for (int j = 0; j < this->m_cols; j++)
        {
            (*this)(i, j) = op((*this)(i, j), other(i, j)); 
        }
    }
}

然后像这样使用它

// Add two matrices
Matrix& operator+=(const Matrix& other)
{
    loops(other, std::plus{});
    return *this;
}

// Subtract two matrices
Matrix& operator-=(const Matrix& other)
{   
    loops(other, std::minus{});
    return *this;
}

3

我认为这种问题通常意味着抽象不好。

在这个例子中,一个高效的矩阵将拥有一个单一连续的数组,其中matrix(i,j)被转换为幕后的array[i*n_columns+j]。在许多情况下,i,j接口更简单(否则你只需使用向量),但没有理由限制用户直接访问底层数组元素 - 更不用说在你自己的矩阵类内部了!

另一种说法是,你正在使用创建(i,j)抽象的类,现在你想要取消工作的另一层抽象。这会耗费CPU时间和你的时间,并且会产生混乱的代码。相反,确保你(和你的用户)可以通过直接元素访问和迭代器访问底层数组:

public:

auto & operator [] (size_t i)
{
    return data[i]; // our underlying array
}

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)
{
    auto it1 = begin();
    auto it2 = other.begin();

    for (size_t i = 0; i < size(); ++i)
        it1[i] += it2[i];

    return *this;
}

Matrix& operator -= (const Matrix& other)
{   
    auto it1 = begin();
    auto it2 = other.begin();

    for (size_t i = 0; i < size(); ++i)
        it1[i] -= it2[i];

    return *this;
}

好了,这样做比较好,但我们可以将任何迭代器或容器的重复部分抽象出来,像这样:

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;
}

我认为这里的教训是永远不要撤消抽象化的工作;如果它对你的代码的某个部分没有用处,那就完全避免使用它(在那部分代码中)。取消一层抽象化需要另外一层抽象化的支持,这就是为什么有些人会放弃面向对象编程的原因。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接