【问题标题】:c++ template class's operatorsc++模板类的操作符
【发布时间】:2011-11-08 01:55:56
【问题描述】:

我通过创建自己的数据结构类(准确地说是矩阵)自学 C++,并且我已将其从仅使用双精度类型更改为 <T> 类型的模板类。重载的矩阵运算符非常标准

    // A snippet of code from when this matrix wasn't a template class
    // Assignment
    Matrix& operator=( const Matrix& other );

    // Compound assignment
    Matrix& operator+=( const Matrix& other ); // matrix addition
    Matrix& operator-=( const Matrix& other ); // matrix subtracton
    Matrix& operator&=( const Matrix& other ); // elem by elem product
    Matrix& operator*=( const Matrix& other ); // matrix product

    // Binary, defined in terms of compound
    Matrix& operator+( const Matrix& other ) const; // matrix addition
    Matrix& operator-( const Matrix& other ) const; // matrix subtracton
    Matrix& operator&( const Matrix& other ) const; // elem by elem product
    Matrix& operator*( const Matrix& other ) const; // matrix product

    // examples of += and +, others similar
    Matrix& Matrix::operator+=( const Matrix& rhs )
    {
        for( unsigned int i = 0; i < getCols()*getRows(); i++ )
        {
            this->elements.at(i) += rhs.elements.at(i);
        }
        return *this;
    }

    Matrix& Matrix::operator+( const Matrix& rhs ) const
    {
        return Matrix(*this) += rhs;
    }

但是现在 Matrix 可以有一个类型,我无法确定哪些矩阵引用应该是类型 &lt;T&gt; 以及后果会是什么。我是否应该允许不同的类型相互操作(例如,Matrix&lt;foo&gt; a + Matrix&lt;bar&gt; b 是有效的)?我也有点模糊

我对不同类型感兴趣的一个原因是便于将来使用复数。我是 C++ 的新手,但我很乐意潜心学习。如果您熟悉任何处理此问题的免费在线资源,我会发现这对您最有帮助。

编辑:难怪没有人认为这是有道理的,我在正文中的所有尖括号都被视为标签!我不知道如何转义它们,所以我将它们内联编码。

【问题讨论】:

  • 你可以通过使你的代码明显是 C++ 代码来“逃避”它们,从 template&lt;... 行开始。
  • 感谢您的提示,但事实并非如此。我所说的尖括号在问题的正常文本正文中。
  • 哦,那个。我想这很聪明。通常,您在代码或变量名称等内容周围使用反引号(在您的 ~ 键上)使它们成为monospaced

标签: c++ templates data-structures matrix operator-overloading


【解决方案1】:

我认为我应该说明我对参数化矩阵维度的评论,因为您以前可能没有见过这种技术。

template<class T, size_t NRows, size_t NCols>
class Matrix
{public:
    Matrix() {} // `data` gets its default constructor, which for simple types
                // like `float` means uninitialized, just like C.
    Matrix(const T& initialValue)
    {   // extra braces omitted for brevity.
        for(size_t i = 0; i < NRows; ++i)
            for(size_t j = 0; j < NCols; ++j)
                data[i][j] = initialValue;
    }
    template<class U>
    Matrix(const Matrix<U, NRows, NCols>& original)
    {
        for(size_t i = 0; i < NRows; ++i)
            for(size_t j = 0; j < NCols; ++j)
                data[i][j] = T(original.data[i][j]);
    }

private:
    T data[NRows][NCols];

public:
    // Matrix copy -- ONLY valid if dimensions match, else compile error.
    template<class U>
    const Matrix<T, NRows, NCols>& (const Matrix<U, NRows, NCols>& original)
    {
        for(size_t i = 0; i < NRows; ++i)
            for(size_t j = 0; j < NCols; ++j)
                data[i][j] = T(original.data[i][j]);
        return *this;
    }

    // Feel the magic: Matrix multiply only compiles if all dimensions
    // are correct.
    template<class U, size_t NOutCols>
    Matrix<T, NRows, NOutCols> Matrix::operator*(
        const Matrix<T, NCols, NOutCols>& rhs ) const
    {
        Matrix<T, NRows, NOutCols> result;
        for(size_t i = 0; i < NRows; ++i)
            for(size_t j = 0; j < NOutCols; ++j)
            {
                T x = data[i][0] * T(original.data[0][j]);
                for(size_t k = 1; k < NCols; ++k)
                    x += data[i][k] * T(original.data[k][j]);
                result[i][j] = x;
            }
        return result;
    }

};

所以你要声明一个floats 的 2x4 矩阵,初始化为 1.0,如下:

Matrix<float, 2, 4> testArray(1.0);

请注意,由于大小是固定的,因此不需要存储在堆上(即使用operator new)。您可以在堆栈上分配它。

您可以创建另外一对ints 矩阵:

Matrix<int, 2, 4> testArrayIntA(2);
Matrix<int, 4, 2> testArrayIntB(100);

对于复制,尺寸必须匹配,但类型不匹配:

Matrix<float, 2, 4> testArray2(testArrayIntA); // works
Matrix<float, 2, 4> testArray3(testArrayIntB); // compile error
// No implementation for mismatched dimensions.

testArray = testArrayIntA; // works
testArray = testArrayIntB; // compile error, same reason

乘法必须有正确的维度:

Matrix<float, 2, 2> testArrayMult(testArray * testArrayIntB); // works
Matrix<float, 4, 4> testArrayMult2(testArray * testArrayIntB); // compile error
Matrix<float, 4, 4> testArrayMult2(testArrayIntB * testArray); // works

请注意,如果有问题,它会在编译时被捕获。这只有在矩阵尺寸在编译时固定的情况下才有可能。另请注意,此边界检查导致没有额外的运行时代码。如果您只是将尺寸设为常量,则会得到相同的代码。

调整大小

如果您在编译时不知道矩阵尺寸,但必须等到运行时,此代码可能没有多大用处。您必须编写一个在内部存储维度和指向实际数据的指针的类,并且它需要在运行时完成所有操作。提示:编写operator [] 将矩阵视为重新整形的 1xN 或 Nx1 向量,并使用operator () 执行多索引访问。这是因为operator [] 只能带一个参数,而operator () 没有这个限制。尝试支持M[x][y] 语法很容易使自己陷入困境(至少迫使优化器放弃)。

也就是说,如果您需要通过某种标准矩阵调整大小来将一个 Matrix 调整为另一个,假设所有维度在编译时都是已知的,那么您可以编写一个函数来调整大小。例如,此模板函数会将任何Matrix 重塑为列向量:

template<class T, size_t NRows, size_t NCols>
Matrix<T, NRows * NCols, 1> column_vector(const Matrix<T, NRows, NCols>& original)
{   Matrix<T, NRows * NCols, 1> result;

    for(size_t i = 0; i < NRows; ++i)
        for(size_t j = 0; j < NCols; ++j)
            result.data[i * NCols + j][0] = original.data[i][j];

    // Or use the following if you want to be sure things are really optimized.
    /*for(size_t i = 0; i < NRows * NCols; ++i)
        static_cast<T*>(result.data)[i] = static_cast<T*>(original.data)[i];
    */
    // (It could be reinterpret_cast instead of static_cast. I haven't tested
    // this. Note that the optimizer may be smart enough to generate the same
    // code for both versions. Test yours to be sure; if they generate the
    // same code, prefer the more legible earlier version.)

    return result;
}

...好吧,无论如何,我认为这是一个列向量。希望如果没有,如何解决它是显而易见的。无论如何,优化器会看到你返回result 并删除额外的复制操作,基本上是在调用者想要看到的地方构建结果。

编译时维度健全性检查

假设如果维度为0(通常导致Matrix 为空),我们希望编译器停止。我听说过一种叫做“编译时断言”的技巧,它使用模板特化并声明为:

template<bool Test> struct compiler_assert;
template<> struct compiler_assert<true> {};

它的作用是让你编写如下代码:

private:
    static const compiler_assert<(NRows > 0)> test_row_count;
    static const compiler_assert<(NCols > 0)> test_col_count;

基本思想是,如果条件为true,则模板将变为空的struct,没有人使用并被默默丢弃。但是如果是false,编译器就找不到struct compiler_assert&lt;false&gt;定义(仅仅一个声明,这还不够)并且会出错。 p>

更好的是 Andrei Alexandrescu 的版本(来自 his book),它允许您使用声明对象的声明名称作为即兴错误消息:

template<bool> struct CompileTimeChecker
{ CompileTimeChecker(...); };
template<> struct CompileTimeChecker<false> {};
#define STATIC_CHECK(expr, msg) { class ERROR_##msg {}; \
    (void)sizeof(CompileTimeChecker<(expr)>(ERROR_##msg())); }

您为msg 填写的内容必须是有效的标识符(仅限字母、数字和下划线),但这没什么大不了的。然后我们只需将默认构造函数替换为:

Matrix()
{   // `data` gets its default constructor, which for simple types
    // like `float` means uninitialized, just like C.
    STATIC_CHECK(NRows > 0, NRows_Is_Zero);
    STATIC_CHECK(NCols > 0, NCols_Is_Zero);
}

瞧,如果我们错误地将其中一个维度设置为0,编译器就会停止。有关它的工作原理,请参阅Andrei's book 的第 25 页。请注意,在true 的情况下,只要测试没有副作用,生成的代码就会被丢弃,因此不会出现膨胀。

【讨论】:

  • 非常感谢你写得很好的例子。我以前从未见过。只是想知道,这是否使调整矩阵的大小变得不可能?还是只需要使用一些我不知道的 C++ foo?
  • 这意味着Matrix 的给定实例化 具有固定大小。这就是让编译器进行优化所付出的代价。顺便说一句,有一个称为“模板专业化”的功能可以让您编写用于此模板的某些版本的代码。例如,如果您为 4x4 float 矩阵优化了汇编代码(例如 SSE3),您可以让编译器用该代码替换 Matrix&lt;float, 4, 4&gt; 的实例。否则,它将使用上面的模板代码。它用途广泛。
【解决方案2】:
Matrix<double> x = ...;
Matrix<int> y = ...;
cout << x + y << endl; // prints a Matrix<double>?

好的,这是可行的,但问题很快就会变得棘手。

Matrix<double> x = ...
Matrix<complex<float>> y = ...
cout << x + y << endl; // Matrix<complex<double>>?

如果您要求您的二元运算符使用类似类型的操作数并强制您的应用程序构建者显式地类型转换它们的值,那么您很可能会最开心。对于后一种情况:

cout << ((Matrix<complex<double>>) x) + ((Matrix<complex<double>>) y) << endl;

您可以提供成员模板构造函数(或类型转换运算符)来支持转换。

template <typename T>
class Matrix {
   ...
public:
   template <typename U>
   Matrix(const Matrix<U>& that) { 
       // initialize this by performing U->T conversions for each element in that
   }
   ...
};

另一种方法是让您的二元运算符模板根据两个操作数的元素类型推断出正确的 Matrix 返回类型,这需要一些适度复杂的模板元编程,这可能不是您想要的。

【讨论】:

  • 并将运算符更改为如下所示: Matrix& operator+=( const Matrix& other );会成功吗?
  • @user:我认为转换构造函数最好是explicit,并且用户调用转换构造函数而不是强制转换。例如:cout &lt;&lt; Matrix&lt; complex&lt;double&gt; &gt;(x) + Matrix&lt; complex&lt;double&gt; &gt;(y) &lt;&lt; endl; 允许隐式转换会导致难以追踪的错误。
  • ...这就是为什么我认为该语言应该被设计为默认构造函数是显式的,并且您需要添加 implicit 关键字以允许隐式转换。
【解决方案3】:

我不确定我明白你在问什么。

但我要指出,您的运营商声明不正确和/或不完整。

首先,赋值运算符应该返回与其参数相同的类型;即:

const Matrix& operator=(const Matrix& src);

其次,二元运算符返回一个新对象,因此您不能返回引用。所有的二元运算符都应该这样声明:

Matrix operator+( const Matrix& other ) const; // matrix addition
Matrix operator-( const Matrix& other ) const; // matrix subtracton
Matrix operator&( const Matrix& other ) const; // elem by elem product
Matrix operator*( const Matrix& other ) const; // matrix product

实际上,将二元运算符声明和实现为全局友元函数被认为是更好的风格:

class Matrix { ... };

inline Matrix operator+(const Matrix& lhs,const Matrix& rhs)
{ return Matrix(lhs)+=rhs; }

希望这会有所帮助。


现在我明白你在问什么了。

在这种情况下,您对各种运算符的实现可能包括对复合类型的操作。因此,Matrix op Matrix 是否有意义的问题实际上取决于 string op int 是否有意义(以及这样的事情是否有用)。您还需要确定返回类型可能是什么。

假设返回类型与 LHS 操作数相同,则声明如下所示:

template <typename T>
class Matrix
{
    template <typename U>
    Matrix<T>&  operator+=(const Matrix<U>& rhs);
};

template <typename T,typename U>
Matrix<T> operator+(const Matrix<T>& lhs,const Matrix<U>& rhs)
{ return Matrix<T>(lhs)+=rhs; }

【讨论】:

  • const Matrix&amp; operator=(const Matrix&amp; src); 可以工作,但返回非常量引用是普遍接受的赋值运算符。例如,查看SGI's string implementation
  • 糟糕,复制粘贴出卖了我;我意识到二进制文件不会返回参考。我想知道的是允许变量类型将如何影响运算符。例如,如果有人制作了一个 Matrix 并尝试添加一个 Matrix 怎么办?我应该尝试通过使用 Matrix operator+( const Matrix& other) 来阻止这种情况吗?
【解决方案4】:

您根本不需要添加太多,因为在模板内部,类名本身就是指当前模板参数。所以以下是等价的:

template <typename T> struct Foo
{
  Foo<T> bar(const Foo<T> &);
  Foo bar2(const Foo *);       // same
};

因此,您的所有操作都无需更改即可完成。您应该添加的是一个将一种矩阵类型转换为另一种矩阵类型的构造函数:

temlate <typename T> class Matrix
{
  template <typename U> Matrix(const Matrix<U> &);  // construct from another matrix
  /*...*/
};

使用该转换构造函数,您可以在运算符中混合矩阵,因为Matrix&lt;T&gt;::operator+(Matrix&lt;U&gt;) 将使用转换创建Matrix&lt;T&gt; 类型的参数,然后您使用已经实现的运算符。

在 C++11 中,您可以将 static_assert(std::is_convertible&lt;U, T&gt;::value, "Boo"); 添加到转换构造函数中,以便在使用不兼容的类型调用它时为您提供有用的编译时诊断。

【讨论】:

  • 另外,我不确定允许从另一个 Matrix 类型进行隐式转换是否被认为是好的做法。我认为从另一个 Matrix 类型构造 explicit 更好,这样在涉及不同 Matrix 类型的表达式中就不会出现意外。
【解决方案5】:

首先,复制赋值运算符不应该有const Matrix&amp;作为它的返回类型;你的界面是正确的。

Grant 关于如何实现二元运算符的建议是做这些事情的普遍接受的方式。

这是一个很好的练习,但很快就会明白为什么在 C++ 中做线性代数是一个坏主意。像A+BA*B 这样的操作只有在矩阵的维度匹配时才有效。

【讨论】:

  • 我不知道为什么边界检查很重要。实现起来并不难。
  • 边界检查,如线程安全,最好通过策略类来实现。此外,如果维度是在编译时通过模板参数设置的,而不是像std::vector 那样留给运行时,则可以在编译时检查边界检查和运算符兼容性,并且生成的目标代码将具有与 if 相同的性能它是硬编码的。真的很酷。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-04
  • 1970-01-01
  • 1970-01-01
  • 2016-06-09
  • 2019-03-14
相关资源
最近更新 更多