2011-09-01 44 views
1

我通过创建自己的数据结构类(准确地说是一个矩阵)来教自己创建C++,并且我仅将它改为仅使用双精度的<T>类型的模板类。重载的矩阵运算符是相当标准C++模板类的运算符

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

但现在矩阵可以有一个类型,我无法确定该矩阵的参考应该是<T>型,会是什么后果。我应该允许不同的类型相互操作(例如,Matrix <foo> a + Matrix <bar> b有效)?我也有点模糊如何

我对不同类型感兴趣的一个原因是为了方便将来使用复数。我是C++的新手,但很高兴潜入我的头脑中学习。如果您熟悉解决此问题的免费在线资源,我会发现最有帮助的。

编辑:难怪没有人认为这是有道理的我身体的所有尖括号被视为标签!我无法弄清楚如何逃脱它们,所以我会内联代码。

+0

通过从'template <...'行开始,让你的代码显然成为C++代码,就可以“逃避”它们。 –

+0

感谢您的提示,但事实并非如此。我正在谈论的尖括号是在问题的正常文本主体中。 –

+0

哦,那个。我想这很聪明。通常情况下,你在代码或变量名称周围使用反引号(在你的〜键上)来使它们成为'等宽'。 –

回答

2

我想我应该说明我关于参数化矩阵尺寸的评论,因为你以前可能没有看过这种技术。

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

}; 

所以,你会申报的float个2×4矩阵,初始化为1.0,如:

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

注意,没有用于存储是在堆(即使用operator new没有要求),因为大小是固定的。你可以在堆栈上分配它。

您可以创建int是另一个夫妇矩阵:

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 

请注意,如果有一个botch,它在编译时被捕获。但是,只有在编译时固定矩阵尺寸的情况下才可能。还要注意,这个边界检查结果在没有额外的运行时代码。如果您只是将维数设为常量,您将得到相同的代码。

调整大小

如果你不知道在编译时你的矩阵尺寸,但必须等到运行时,该代码可能没有多少用处。您必须编写一个内部存储维度的类和一个指向实际数据的指针,并且它需要在运行时执行所有操作。提示:编写您的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<false>一个定义(只是声明,这是不够的),并出现了错误。

更好的是安德烈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的情况下,只要测试没有副作用,生成的代码就会被丢弃,所以没有膨胀。

+0

非常感谢这个写得很好的例子。我以前从来没有见过。只是想知道,这是否让Matrix无法调整大小?或者你只需​​要使用一些我不知道的C++ foo? –

+0

这意味着Matrix的给定*实例*具有固定的大小。这是您让编译器进行优化所付出的代价。顺便说一句,有一个称为“模板专业化”的功能,可以让您编写代码以用于此模板的某些版本。例如,如果您为4x4浮点矩阵优化了汇编代码(例如SSE3),则可以让编译器在该代码中替换“Matrix '的实例。否则,它将使用上面的模板代码。它非常灵活。 –

1

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

但我会指出你的运算符声明不正确和/或不完整。

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

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对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; } 
+0

'常量矩阵运算符=(常量矩阵& src);'将工作,但返回一个非const引用是普遍接受的赋值运算。对于一个例子,检查出[SGI的字符串实现](http://www.sgi.com/高科技/ STL/basic_string.html) –

+0

哎呀,复制粘贴背叛了我,我知道,二进制文件没有返回参考我想知道的是如何让一个变量的类型会影响到运营商例如,如果有人做了什么。矩阵,并试图添加一个矩阵?我应该尝试通过矩阵运算符+(常量矩阵及其他)来阻止该问题吗? –

0

首先,拷贝赋值操作符不应该有const Matrix&作为它的返回类型;你的界面是正确的。

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

这是一个很好的练习,但很快就会看到为什么在C++中使用线性代数是一个不好的主意。像A+BA*B这样的操作仅在矩阵的维数匹配时才有效。

+0

我不知道为什么边界检查是一个大问题。 。 –

+0

边界检查,如线程安全,最好通过策略类来实现。此外,如果维度设置为通过模板参数编译时间,而不是像'std :: vector'那样将其留在运行时,然后可以在编译时检查边界检查和运算符兼容性,并且生成的目标代码将具有与硬编码相同的性能。这真的很酷。 –

0

根本不需要添加太多内容,因为在模板中,类名称本身引用了当前的模板参数。所以下面是等效的:

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<T>::operator+(Matrix<U>)将使用转换创建Matrix<T>类型的参数,并那么你使用你已经实现的操作符。

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

+1

另外,我不确定是否允许从另一个Matrix类型进行隐式转换将被认为是良好的做法。我认为另一种矩阵类型的构造是“明确的”,以便在涉及不同矩阵类型的表达式中不会有任何意外。 –

+0

@艾米尔:是的,好主意,绝对值得考虑。 –

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

另一种选择,让您的二元运算模板推导基于元素类型的两个操作数的正确矩阵返回类型,需要一些比较复杂的模板元编程,你正在寻找进入可能不是什么。

+0

并改变运营商看起来像这样:矩阵&运算符+ =(常量矩阵和其他);会做的伎俩? –

+0

@user:我认为最好是转换构造函数是'explicit',并且用户调用转换构造函数而不是转换。例如:'cout << Matrix >(x)+ Matrix >(y)<< endl;'允许隐式转换会导致难以追踪的bug。 –

+0

...这就是为什么我认为语言应该被设计为默认情况下构造函数是显式的,并且您需要添加一个“隐式”关键字以允许隐式转换。 –