C++ 运算符 Modification/Metaprogramming 简化语法的策略

C++ Operator Modification/Metaprogramming Strategies for Less-Verbose Syntax

我现在正在学习 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????
}
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);

None 其中的语法似乎与 Matlab 的一样优雅:

mat1 .* mat2;

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

这是我对此的认识:

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

对于这两项,我选择实现一个 n 维数组 class(因为 CRTP 基础 classes 和自由函数用于实现,这只是提供的问题具有必要的 traits/alias 声明的空结构)。

以下情况会导致阿达玛乘法:

  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 表达式可以显式转换为数组,从而产生阿达玛乘法

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

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

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

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