关于C++ 的默认函数
默认构造函数,拷贝构造,赋值拷贝,移动拷贝,移动赋值拷贝,默认析构函数。移动拷贝函数和移动赋值拷贝是后面新加的,移动构造函数使右值对象拥有的资源无需赋值即可移动到左值中。
目前的经验来看,这三者主要是应对深拷贝,浅拷贝的问题。这类场景通常是对象持有一个数据指针。
浅拷贝,直接赋值数据指针,那就有两个指针指向同一份数据,这个就引入了很多风险,比如重复释放导致Crash 的问题。
深拷贝,为了解决两个不同的对象持有同一份数据,深拷贝直接把数据复制一份,这样两个对象的数据就没有关联了。但是这样做的缺点就是资源消耗大,比如cv::Mat 的 clone操作。
移动操作,就是把数据直接 move到目标对象,就像剪切粘贴一样,把自己的数据赋值给目标对象,然后把自己的指针置为空。这样就避免了两个对象指向同一份数据的情况。但是这也将导致源数据失效,所有移动也是有代价的,具体取决于场景,比如一些临时对象,就可以用移动操作来转移自己的数据。
大概长这个样子:
cpp
#include <iostream>
#include <algorithm>
#include <vector>
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: _length(length)
, _data(new int[length])
{
std::cout << "In memory block constructor, length = "
<< _length << "." << std::endl;
}
// Destructor.
~MemoryBlock()
{
std::cout << "In memory block destructor, length = "
<< _length << ".";
if (_data != nullptr)
{
std::cout << " Deleting resource.";
// Delete the resource.
delete[] _data;
}
std::cout << std::endl;
}
// Copy constructor.
MemoryBlock(const MemoryBlock& othre)
: _length(othre._length)
, _data(new int[othre._length])
{
std::cout << "In memory block copy constructor, length = "
<< othre._length << ". Copying resource." << std::endl;
std::copy(othre._data, othre._data + _length, _data);
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
std::cout << "In memory block copy assignment operator, length = "
<< other._length << ". Copying resource." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
// Move copy constructor.
MemoryBlock(MemoryBlock&& other)
: _data(nullptr)
, _length(0)
{
std::cout << "In memory block move constructor, length = "
<< other._length << ". Moving resource." << std::endl;
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
std::cout << "In memory block move assignment operator, length = "
<< other._length << ". Moving resource." << std::endl;
if (this != &other)
{
delete[] _data;
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
return *this;
}
// Retrieves the length of the data resource.
size_t Length() const {
return _length;
}
private:
size_t _length; // The length of the resource.
int* _data; // The resource.
};
int main()
{
// Use move constructor improve performance
// Create a vector object and add a few elements to it.
std::vector<MemoryBlock> v;
v.push_back(MemoryBlock(25));
v.push_back(MemoryBlock(75));
// Insert a new element into the second position of the vector.
v.insert(v.begin() + 1, MemoryBlock(50));
return 0;
}
OpenCV Mat
在OpenCV 的 Mat结构体中,主要包括两部分,一个是Mat的宽高通道等mat信息,一个是真正的Mat数据指针,指向实际的数据位置。这里就涉及到浅拷贝,深拷贝和移动赋值的问题了,比如
cpp
cv::Mat img = cv::imread(imgPath);
cv::Mat img1(img); // 浅拷贝,引用计数 + 1
cv::Mat img2 = img // 浅拷贝,引用计数 + 1
cv::Mat img2 = img.clone(); // 深拷贝,直接把数据复制一份
这里主要讨论 Mat 的构造和赋值方法,忽略Mat本身的一些细节。
https://docs.opencv.org/4.x/d3/d63/classcv_1_1Mat.html
Mat 类成员变量
cpp
/*! includes several bit-fields:
- the magic signature
- continuity flag
- depth
- number of channels
*/
int flags;
//! the matrix dimensionality, >= 2
int dims;
//! the number of rows and columns or (-1, -1) when the matrix has more than 2 dimensions
int rows, cols;
//! pointer to the data
uchar* data;
//! helper fields used in locateROI and adjustROI
const uchar* datastart;
const uchar* dataend;
const uchar* datalimit;
//! custom allocator
MatAllocator* allocator;
//! and the standard allocator
static MatAllocator* getStdAllocator();
static MatAllocator* getDefaultAllocator();
static void setDefaultAllocator(MatAllocator* allocator);
//! internal use method: updates the continuity flag
void updateContinuityFlag();
//! interaction with UMat
UMatData* u;
MatSize size;
MatStep step;
这里解释一下什么是 Mat 的 step. Mat 中的step 主要表示矩阵的一行占的字节数,这个值包括每行末尾的填充字节数(如果有的话)。
构造函数
cpp
Mat::Mat() CV_NOEXCEPT
: flags(MAGIC_VAL), dims(0), rows(0), cols(0), data(0), datastart(0), dataend(0),
datalimit(0), allocator(0), u(0), size(&rows), step(0)
{}
void Mat::create(int _rows, int _cols, int _type)
{
_type &= TYPE_MASK;
if( dims <= 2 && rows == _rows && cols == _cols && type() == _type && data )
return;
int sz[] = {_rows, _cols};
create(2, sz, _type);
release
cpp
void Mat::release()
{
// 这里是有数据,并且refcount = 2 的时候就开始释放
if( u && CV_XADD(&u->refcount, -1) == 1 )
deallocate();
u = NULL;
datastart = dataend = datalimit = data = 0;
for(int i = 0; i < dims; i++)
size.p[i] = 0;
#ifdef _DEBUG
flags = MAGIC_VAL;
dims = rows = cols = 0;
if(step.p != step.buf)
{
fastFree(step.p);
step.p = step.buf;
size.p = &rows;
}
#endif
}
void Mat::deallocate()
{
if(u)
{
UMatData* u_ = u;
u = NULL;
(u_->currAllocator ? u_->currAllocator : allocator ? allocator : getDefaultAllocator())->unmap(u_);
}
}
void deallocate(UMatData* u) const CV_OVERRIDE
{
if(!u)
return;
CV_Assert(u->urefcount == 0);
CV_Assert(u->refcount == 0);
if( !(u->flags & UMatData::USER_ALLOCATED) )
{
fastFree(u->origdata);
u->origdata = 0;
}
delete u;
}
void fastFree(void* ptr)
#endif
{
#if defined HAVE_POSIX_MEMALIGN || defined HAVE_MEMALIGN
if (isAlignedAllocationEnabled())
{
free(ptr);
return;
}
#elif defined HAVE_WIN32_ALIGNED_MALLOC
if (isAlignedAllocationEnabled())
{
_aligned_free(ptr);
return;
}
#endif
if(ptr)
{
uchar* udata = ((uchar**)ptr)[-1];
CV_DbgAssert(udata < (uchar*)ptr &&
((uchar*)ptr - udata) <= (ptrdiff_t)(sizeof(void*)+CV_MALLOC_ALIGN));
free(udata);
}
}
赋值拷贝函数
cpp
Mat& Mat::operator=(const Mat& m)
{
if( this != &m )
{
if( m.u )
CV_XADD(&m.u->refcount, 1);
// 这个release 是释放自身的,也就是说会把当前的数据给释放掉
release();
flags = m.flags;
if( dims <= 2 && m.dims <= 2 )
{
dims = m.dims;
rows = m.rows;
cols = m.cols;
step[0] = m.step[0];
step[1] = m.step[1];
}
else
copySize(m);
data = m.data;
datastart = m.datastart;
dataend = m.dataend;
datalimit = m.datalimit;
allocator = m.allocator;
u = m.u;
}
return *this;
}
移动赋值来拷贝函数
cpp
Mat& Mat::operator=(Mat&& m)
{
if (this == &m)
return *this;
release();
flags = m.flags; dims = m.dims; rows = m.rows; cols = m.cols; data = m.data;
datastart = m.datastart; dataend = m.dataend; datalimit = m.datalimit; allocator = m.allocator;
u = m.u;
if (step.p != step.buf) // release self step/size
{
fastFree(step.p);
step.p = step.buf;
size.p = &rows;
}
if (m.dims <= 2) // move new step/size info
{
step[0] = m.step[0];
step[1] = m.step[1];
}
else
{
CV_Assert(m.step.p != m.step.buf);
step.p = m.step.p;
size.p = m.size.p;
m.step.p = m.step.buf;
m.size.p = &m.rows;
}
// 移动赋值,会把_src的数据置为空
m.flags = MAGIC_VAL; m.dims = m.rows = m.cols = 0;
m.data = NULL; m.datastart = NULL; m.dataend = NULL; m.datalimit = NULL;
m.allocator = NULL;
m.u = NULL;
return *this;
}
assignTo
cpp
void Mat::assignTo( Mat& m, int _type = -1) const
{
// 如果不转类型,相当于直接浅拷贝,引用计数不加1.
if( _type < 0 )
m = *this;
else
convertTo(m, _type);
}
copyTo
cpp
void Mat::copyTo( OutputArray _dst ) const
{
CV_INSTRUMENT_REGION();
#ifdef HAVE_CUDA
if (_dst.isGpuMat())
{
_dst.getGpuMat().upload(*this);
return;
}
#endif
int dtype = _dst.type();
if( _dst.fixedType() && dtype != type() )
{
CV_Assert( channels() == CV_MAT_CN(dtype) );
convertTo( _dst, dtype );
return;
}
if( empty() )
{
_dst.release();
return;
}
if( _dst.isUMat() )
{
// 如果 _dst 的size 和 type 和 _src 一样 则不需要申请。否则需要重新申请。
_dst.create( dims, size.p, type() );
UMat dst = _dst.getUMat();
CV_Assert(dst.u != NULL);
size_t i, sz[CV_MAX_DIM] = {0}, dstofs[CV_MAX_DIM], esz = elemSize();
CV_Assert(dims > 0 && dims < CV_MAX_DIM);
for( i = 0; i < (size_t)dims; i++ )
sz[i] = size.p[i];
sz[dims-1] *= esz;
dst.ndoffset(dstofs);
dstofs[dims-1] *= esz;
dst.u->currAllocator->upload(dst.u, data, dims, sz, dstofs, dst.step.p, step.p);
return;
}
if( dims <= 2 )
{
_dst.create( rows, cols, type() );
Mat dst = _dst.getMat();
if( data == dst.data )
return;
if( rows > 0 && cols > 0 )
{
Mat src = *this;
Size sz = getContinuousSize2D(src, dst, (int)elemSize());
CV_CheckGE(sz.width, 0, "");
const uchar* sptr = src.data;
uchar* dptr = dst.data;
#if IPP_VERSION_X100 >= 201700
CV_IPP_RUN_FAST(CV_INSTRUMENT_FUN_IPP(ippiCopy_8u_C1R_L, sptr, (int)src.step, dptr, (int)dst.step, ippiSizeL(sz.width, sz.height)) >= 0)
#endif
for (; sz.height--; sptr += src.step, dptr += dst.step)
memcpy(dptr, sptr, sz.width);
}
return;
}
_dst.create( dims, size, type() );
Mat dst = _dst.getMat();
if( data == dst.data )
return;
if( total() != 0 )
{
const Mat* arrays[] = { this, &dst };
uchar* ptrs[2] = {};
NAryMatIterator it(arrays, ptrs, 2);
size_t sz = it.size*elemSize();
for( size_t i = 0; i < it.nplanes; i++, ++it )
memcpy(ptrs[1], ptrs[0], sz);
}
}
copyTo 用法1
如果 _dst 的size 和 type 和 _src 一样 则不需要申请。否则需要重新申请。所以在实际中我们使用
cpp
// 如果_dst 是一个局部变量,那每次拷贝都需要申请一块内存
// 如果_dst 是一个生命周期更长的变量,那每次拷贝就不需要申请内存。实际测试中,这种会省很多时间。
frame.copyTo(_dst);
copyTo 用法2
cpp
auto frameAuto = cv::Mat(1080, 720, CV_8SC3); // 384us
frameCopyTo = cv::Mat(1080, 720, CV_8SC3); // 339us
frameCopyToGlobal = cv::Mat(1080, 720, CV_8SC3); // 507us
cv::Mat(1080, 720, CV_8SC3).copyTo(frameCopyToGlobal); // 647us
为什么给全局的Mat 赋值会耗时更多,因为如果_dst Mat 不为空,切引用为1, 就会多一次对 _dst 的释放。
线程安全
在这里cv::Mat 引入了引用计数的技术,在赋值或浅拷贝的时候,只拷贝mat信息,不拷贝mat 数据。当 引用计数为0的时候,就释放数据。