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

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

提问人:StillUsesFORTRAN 提问时间:2/19/2021 更新时间:2/19/2021 访问量:789

问:

我正在创建一个矩阵模板,在编写复制构造函数时遇到了问题。虽然数据似乎是从构造函数中正确复制的,但返回到主程序的对象没有正确的值(看起来像它指向不同的内存地址)。在我尝试调试它时,我尝试创建一个极简主义示例,但奇怪的是,这并没有产生相同的错误。我感觉这个问题要么超出了我对 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 的学习练习。一旦这更发达,我可能会添加绑定检查。

C++ 数组 复制构造函数 C C++17

评论

1赞 Lukas-T 2/19/2021
您永远不会为 . 分配任何内容。你只声明一堆同名的局部变量。Matrix::data
1赞 Human-Compiler 2/19/2021
未调整大小的数组成员不是标准的 C++,也不是从运行时值初始化它,例如 -- 这是可变长度数组,并且是编译器给出的非标准扩展。如果你使用这样的功能,那么你一开始就没有编写普通/标准的C++T data[]T data[m*n];
0赞 StillUsesFORTRAN 2/19/2021
我需要在构造函数中分配空间吗?像这样:data = new T[m*n];

答:

3赞 Human-Compiler 2/19/2021 #1

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


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

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

1. 灵活的阵列成员

您使用的第一个编译器扩展是 中称为灵活数组的功能

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

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

这应该被改掉,以更连贯地改变。

2. 可变长度数组

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

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

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

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

建议的修复

  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::vectorvectorresizereservepush_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;
    

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


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

我鼓励你不要回避容器,就像纯粹为了 HPC 的目的一样。std::vector

坦率地说,开发人员在确定什么对性能有好处和什么对性能不利方面是出了名的糟糕。底层硬件在推测执行、分支预测、指令流水线和缓存位置等因素中起着最大的作用。堆内存和一些额外的字节拷贝通常是您最不担心的,除非您在非常紧密的循环中反复增加容器。

相反,堆内存很容易移动(例如移动构造函数),因为它是一个指针复制,而缓冲区存储即使对于移动也会被全部复制。此外, 还引入了具有不同选项的多态分配器,其中内存资源来自这些选项,从而允许更快的分配选项(例如,为内存资源分配整页的虚拟内存资源)。std::vector

即使在性能很重要的情况下:在尝试优化解决方案之前,先尝试解决方案和配置文件。不要在前期浪费精力,因为结果可能会让你大吃一惊。有时,在适当的条件下,做更多的工作可以加快代码速度。

评论

1赞 StillUsesFORTRAN 2/19/2021
@Human-Compiler 感谢您的详细解释和多重建议。作为新的 C++ 用户,这告诉我更多关于代码是如何解释的,而不是将其扫地出门,即使这是更好的方法。std::vector
0赞 Human-Compiler 2/19/2021
@StillUsesFORTRAN 很乐意帮忙。虽然我有点困惑为什么在出于某种原因更改之前我被选为正确答案。对于 99% 的开发人员来说,手动管理内存不是一个好的或可持续的建议
0赞 StillUsesFORTRAN 2/19/2021
对不起,我没有意识到我只能“接受”一个答案。两者都对理解记忆中发生的事情非常有帮助,随着我对语言的适应程度越来越高,我希望非常清楚这一点。
0赞 StillUsesFORTRAN 2/19/2021
快速跟进:没有在堆内存中分配?std::vector
1赞 Human-Compiler 2/19/2021
即使对于高性能计算,堆的使用也不是需要回避的事情。坦率地说,开发人员对什么是“高性能”,以及性能在哪里很重要,直觉是出了名的糟糕。在几乎所有情况下,推理执行、缓存位置和分支预测都比堆分配的成本发挥更大的作用。如果性能很重要,请分析 - 然后做出决定。结果可能会让你大吃一惊
1赞 m88 2/19/2021 #2

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

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

评论

0赞 Human-Compiler 2/19/2021
c++11 已经问世了 10 年,如果算上 TR1,时间会更长。强烈建议在代码中的任何时候使用而不是建议原始/。手动管理内存极易出错。微小的更改可能会引入可能导致泄漏的潜在异常状态,并且不值得使用开销为 0 的智能指针 - 或者更好的是,它为您管理这一切std::unique_ptr<T[]>newdeletestd::vector
0赞 m88 2/19/2021
完全同意。话虽如此,OP 想要一些没有的东西(或类似的 STL 东西,我猜)。std::vector