具有数组属性的 class 的 C++ 复制构造函数

C++ copy constructor for a class with an array attribute

我正在创建一个矩阵模板,运行在编写复制构造函数时遇到了问题。虽然数据似乎从构造函数中正确复制,但返回到主程序的对象没有正确的值(看起来它指向不同的内存地址)。在尝试对此进行调试时,我尝试创建一个极简示例,尽管 st运行gely 这并没有产生相同的错误。我觉得这个问题要么超出了我对 C++ 的理解......要么是由我不知何故错过的错字引起的。谁能发现我做错了什么?

matlib.h

#ifndef MATLIB_H
#define MATLIB_H
#include <iostream>

    namespace matlib{
        template <typename T>
        struct Matrix {
                unsigned int rows;   //number of rows
                unsigned int cols;   //number of columns
                unsigned int length; //number of elements
                T data[];                  //contents of matrix
    
                /* Constructors */
                Matrix(unsigned int m, unsigned int n) : rows(m), cols(n) {
                    length = m*n;
                    T data[m*n];
                    //::std::cout << "Hello from the null constructor!" << ::std::endl;
                    //::std::cout << "rows = " << rows << ", cols = " << cols << ", length = " << length << ::std::endl;
                }
                
                Matrix(const Matrix<T> &mat) {
                    rows = mat.rows;
                    cols = mat.cols;
                    length = mat.length;
                    T data[mat.length];
                    ::std::cout << "Hello from the copy constructor!" << ::std::endl;                
                    for (int i = 0; i < length; ++i) {
                        data[i] = mat.data[i];
                        ::std::cout << "data[" << i << "] = " << data[i] << ", mat.data[" << i << "] = " << mat.data[i] << ::std::endl;
                    }
                } 
    
                //Single element indexing and assigment
                T& operator() (int i, int j) {
                    return data[ i*this->cols + j ];
                }
    
                T& operator() (unsigned int i, unsigned int j) {
                    return data[ i*this->cols + j ];
                }
    
                //Single element indexing and assigment
                T& operator() (int i) {
                    return data[i];
                }
    
                T& operator() (unsigned int i) {
                    return data[i];
                } 
    
                
        };
    
    }
    
    #endif

testscript.cpp

#include <iostream>
#include "matlib.h"

int main() {
    float arr[7] = {4, 1, 6, 6, 8, 4, 2};
    matlib::Matrix<float> mat1(1,7);

    //Assign values and print
    std::cout << "mat1 = ";
    for (int i = 0; i < mat1.length; ++i) {
        mat1(i) = arr[i];
        std::cout << mat1(i) << " ";
    }
    std::cout << "\n" << std::endl;

    //Copy the object
    matlib::Matrix<float> mat2 = mat1;

    //Print the copied values
    std::cout << "mat2 = ";
    for (int i = 0; i < mat2.length; ++i) {
        std::cout << mat2(i) << " ";
    }
    std::cout << std::endl;

    return 0;
}

控制台输出:

mat1 = 4 1 6 6 8 4 2 

Hello from the copy constructor!
data[0] = 4, mat.data[0] = 4
data[1] = 1, mat.data[1] = 1
data[2] = 6, mat.data[2] = 6
data[3] = 6, mat.data[3] = 6
data[4] = 8, mat.data[4] = 8
data[5] = 4, mat.data[5] = 4
data[6] = 2, mat.data[6] = 2
mat2 = 9.80909e-45 1.4013e-45 9.80909e-45 9.80909e-45 4 1 6 

我敢肯定很多人会建议涉及 'std::vector' 的解决方案,尽管这主要是针对 HPC 的学习练习。一旦开发得更好,我可能会添加绑定检查。

简短版本: 只需使用 std::vector。它将使您的生活更轻松,并且比手动方法有更少的陷阱。


长版:你的代码有两个主要问题:

  1. 您错误地使用了 Flexible-Arrays(这是一个编译器扩展,不是标准的 C++),并且
  2. 您错误地使用了可变长度数组(这也是一个编译器扩展,不是标准的 C++)

1。灵活的数组成员

您使用的第一个编译器扩展是 中的一项功能,称为 flexible arrays:

struct Matrix {
    ...
    T data[];
 // ^~~~~~~~~

允许在 struct 末尾使用未调整大小的数据数组来表示对象,当由 malloc 分配时,这些对象可能在运行时被赋予动态大小。然而,这不是有效的标准 并且不建议使用它,因为它根本不适合 C++ 的分配器模型。

这应该改成更连贯的东西。

2。变长数组

您使用的第二个扩展也来自 ,称为 可变长度数组:

Matrix(unsigned int m, unsigned int n) : rows(m), cols(n) {
    ...
    T data[m*n];

这也不是有效的标准 C++。您不能在 C++ 中从运行时值构造数组——句号。数组在编译时已知,并且仅在编译时已知。

此外,这就是您遇到问题的地方,T data[m*n] 正在创建一个名为 datanew VLA,shadows 灵活数组也命名为 data。因此,您定义 T data[m*n]T data[other.length] 的每个函数实际上是在创建 new 数组,写入它们,然后不对它们执行任何操作。这就是您看到不同地址的原因。

建议的修复

  1. 使用堆内存,也许 std::unique_ptr 可以为您管理事物。在构建时分配大小,在复制时克隆它。

    // Construction
    Matrix(unsigned int m, unsigned int n) : rows(m), cols(n), data(std::make_unique<T[]>(m * n))
    // where 'data' is std::unique_ptr<T[]>
    { ... }
    

    这将需要自定义复制构造函数:

    Matrix(const Matrix& other) : rows(other.rows), cols(other.cols), data(std::make_unique<T[]>(rows * cols)){
        // Copy all elements from 'other' to 'data'
        std::copy_n(other.get(), rows * cols, data.get());
    }
    

    或者,更好的是:

  2. 使用std::vector。它已经知道如何做 lifetime 并使您免于许多陷阱。如果您已经知道 vector 的最大大小,则可以只使用 resizereserve+push_back,这样可以节省重新分配成本。

    Matrix(unsigned int m, unsigned int n) : rows(m), cols(n), data(m * n)     
    // where 'data' is std::vector<T>
    { ... }
    

    使用 std::vector 你可以这样做:

    Matrix(const Matrix& other) = default;
    

    在您的 class 声明中,它将使用 std::vector 的底层复制构造函数。这是 IMO 更好的方法。


关于“高性能计算”的单独说明

我鼓励您不要 纯粹出于 HPC 的目的而回避像 std::vector 这样的容器。

坦率地说,众所周知,开发人员不善于确定什么对性能有益,什么不有益。底层硬件在推测执行、分支预测、指令流水线和缓存局部性等因素中发挥着最大的作用。堆内存和一些额外的字节拷贝通常是你最不担心的,除非你在一个非常紧密的循环中重复地增长容器。

相反,堆内存很容易移动(例如移动构造函数),因为它是指针复制,而缓冲区存储即使移动也会被整体复制。此外, 引入了具有不同选项的多态分配器,内存资源来自何处——允许更快的分配选项(例如,为 std::vector 分配完整页面的虚拟内存资源)。

即使性能很重要:在尝试优化解决方案之前先尝试解决方案和 profile。不要一开始就浪费精力,因为结果可能会让您大吃一惊。有时在合适的条件下more work can result in faster code

我建议将您的动态(取消)分配代码移动到特定的受保护方法中。它将帮助您避免内存泄漏、双重释放、无用的重新分配,并使您的构造函数更具可读性。

template <typename T>
struct Matrix {
    std::size_t rows{0}, cols{0};
    size_t capacity{0};
    T* data{nullptr};
    
    Matrix() = default;
    Matrix(size_t rows, size_t cols): Matrix()
    {
        this->allocate(rows * cols);
        this->rows = rows;
        this->cols = cols;
    }
    
    Matrix(const Matrix<T>& other): Matrix()
    {
        *this = other;
    }
    
    Matrix& operator=(const Matrix<T>& other) 
    {
        if (this != &other) {
            this->allocate(other.length());
            std::copy_n(other.data, other.length(), data);
            this->rows = other.rows;
            this->cols = other.cols;
        }
        return *this;
    }
    
    ~Matrix()
    {
        this->release();
    }
    
    size_t length() const { return rows * cols; }

    // Access
    T& operator()(size_t row, size_t col) { /*TODO*/ }
    const T& operator()(size_t row, size_t col) const { /*TODO*/ }
    
protected:
    void allocate(size_t reqLength) 
    {
        if (data && capacity >= reqLength) return;
        this->release();
        data = new T [reqLength];
        capacity = reqLength;
    }
    
    void release()
    {
        if (data) delete [] data;
        data = nullptr;
        capacity = 0;
    }    
};