1

我现在正在学习 C++ 中的模板元编程和表达式模板,所以作为练习,我正在创建一个线性代数库来练习我正在学习的概念。

到目前为止,我的库为所有可以重载的二元运算符提供了一个完整的非成员运算符重载列表,并且具有易于扩展的相当流畅的界面。然而,我遇到的一个问题是矩阵运算通常有多种变化。例如,对于乘法,有一般矩阵乘法、点积、kroenecker 积、hadamard 积、叉积等等。

在 Matlab 中采用的一种巧妙的解决方法是用于 hadamard 乘法(和 .^、./ 等)的 .* 运算符。在这种情况下,Matlab 语言使用 . 运算符作为 * 运算符的修饰符。但是,我不知道 c++ 语言中有任何机制允许像这样修改运算符。这种行为是否有任何干净的解决方法?

以下是我已经想到的一些事情:

  • 运算符重载允许额外的模板参数。但是,我不完全确定如何在这种情况下利用这一点。例如,一些可能很好的东西(尽管在实践中,我不确定是否有有效的语法来实现这一点):
template<typename lhs_t, typename rhs_t, typename op_t = Gemm>
auto operator*(lhs_t lhs, rhs_t rhs)
{
    ...
}

// Operator template specializations ...

int main()
{
    Matrix<double, 2, 2> mat1(1.0, 2.0, 3.0, 4.0);
    Matrix<double, 2, 2> mat2(1.0, 2.0, 3.0, 4.0);

    mat1 * mat2; // Works
    mat1 *<Hadamard> mat2; // Error!  Syntax????
}
  • 使用 SFINAE/Concepts/if constexpr 和特征来修改二进制表达式类型或包装二进制表达式类型。句法:
Hadamard(mat1 * mat2); // Hadamard wraps or modifies binary expression created by operator*
                       // SFINAE or Concepts used to select the correct routine based on the trait set
  • 创建一个免费的二进制函数。可能的语法:
Hadamard<Multiplication>(mat1, mat2);
Hadamard_Multiplication(mat1, mat2);
  • 使用成员函数。句法:
mat1.hadamard_multiplication(mat2);

这些似乎都没有像 Matlab 那样优雅的语法:

mat1 .* mat2;

是否有任何技术可以接近我可以考虑的运算符修饰符语法?或者任何使使用语法不那么冗长的一般技术?如果没有,是否有任何想法可以在未来的 C++ 版本中包含一些在这里可能有用的东西?

4

1 回答 1

0

这是我对此的认识:

  1. 可以支持多种语法。我不需要选择一个,我只需要实现一种方法并在顶部添加抽象来模仿其他语法(如果需要)。
  2. Hadamard 乘法(和除法等)是数组的默认乘法样式,而 GEMM 是矩阵的默认乘法模式。

有了这两项,我选择实现一个 n 维数组类(因为 CRTP 基类和自由函数用于实现,这只是提供一个带有必要特征/别名声明的空结构的问题)。

以下情况可能会导致 hadamard 乘法:

  1. 矩阵 A 与矩阵 B 具有相同的维度,并且两个矩阵都不是方阵,因此很明显 gemm 无效,但 hadamard 乘法有效。因此,使用概念来覆盖此行为是有意义的。换句话说:

    // Results in hadamard multiplication
    template<object_type lhs_t, object_type rhs_t> requires 
    (is_same_dimensions_v<lhs_t, rhs_t> && !is_special_mult_supported_v<lhs_t, rhs_t>)
    constexpr auto operator*(lhs_t&& lhs, rhs_t&& rhs) noexcept -> mul_expr<lhs_t, rhs_t>
    {
        return { std::forward<lhs_t>(lhs), std::forward<rhs_t>(rhs) };
    }
    
    // Results in general matrix multiplication
    template<typename lhs_t, typename rhs_t> requires is_gemm_supported_v<lhs_t, rhs_t>
    constexpr auto operator*(lhs_t&& lhs, rhs_t&& rhs) noexcept -> gemm_expr<lhs_t, rhs_t>
    {
        return { std::forward<lhs_t>(lhs), std::forward<rhs_t>(rhs) };
    }
    
  2. 如果将 gemm 表达式分配给数组,并且 hadamard 乘法有效,则它会隐式转换为 hadamard 乘法。

    // implicit conversion to hadamard multiplication
    array = a * b;
    
  3. Gemm 表达式可以显式转换为 hadamard 表达式。

    // explicitly create a hadamard multiplication expression
    auto c = static_cast<mul_expr>(a * b);
    
  4. Gemm 表达式可以显式地转换为数组,从而产生 hadamard 乘法

    // explicitly create an array using hadamard multiplication
    auto c = static_cast<array>(a * b);
    
  5. 在同时支持 hadamard 和 gemm 的混合数组/矩阵类型的情况下,选择左侧类型以防止歧义。

     // if a is an array, and b is a matrix, then c is a mult_expr
     // if a is a matrix, and b is an array, then c is a gemm_expr
     auto c = a * b;
    

有了这些规则,为了清楚起见,可以添加 api 级别的抽象:

    // c = hadamard(a * b);
    template<expr_type expr_t> requires std::is_convertible_v<std::decay_t<expr_t>, mul_expr>
    constexpr auto hadamard(expr_t&& expr) noexcept
    {
        return static_cast<mul_expr>(expr);
    }

    // support c = hadamard_mult(a, b); syntax
    template<object_type lhs_t, object_type rhs_t> requires is_same_dimensions_v<lhs_t, rhs_t>
    constexpr auto hadamard_mult(lhs_t&& lhs, rhs_t&& rhs) noexcept
    {
        return hadamard(lhs * rhs);
    }

请注意,我省略了 static_casts 并转述了一些语法来理解这个想法。这里要带走的一个重要认识是,c++ 可以利用类型系统来相当大地简化语法,这就是 matlab 不同的地方。有很多情况下

    c = a * b;

可以(并且应该)导致 hadamard 乘法。此外,类型系统的简单操作很容易导致函数语法很容易支持的情况。

非常感谢上面评论中的你们帮助我集思广益并得出这个结论。由于这里的讨论,我对我的图书馆非常满意。

于 2020-12-17T02:45:06.920 回答