Foundation - Array adding option to work with size_t (#1236)

- Added zero-based constructors (allocation + buffer-reuse wrapping) to `NCollection_Array1` and `NCollection_Array2`.
- Added `size_t`-based `Resize()` overloads (and refactored `NCollection_Array1` resize logic into a shared implementation).
- Added GTest coverage for the new zero-based construction / access / resize behaviors.
This commit is contained in:
Pasukhin Dmitry
2026-04-25 21:41:35 +01:00
committed by GitHub
parent d94283c63f
commit 133fd8e228
4 changed files with 453 additions and 74 deletions
@@ -475,3 +475,175 @@ TEST(NCollection_Array1Test, EmplaceValue_ReplacesExisting)
EXPECT_EQ(200, anArray(2).myA);
EXPECT_EQ(30, anArray(3).myA);
}
// ============================================================================
// Zero-based (size_t) construction mode tests
// ============================================================================
TEST(NCollection_Array1Test, SizeConstructor_AllocatesCorrectly)
{
const size_t aSize = 10;
NCollection_Array1<int> anArray(aSize);
EXPECT_EQ(aSize, anArray.Size());
EXPECT_EQ(0, anArray.Lower());
EXPECT_EQ(static_cast<int>(aSize) - 1, anArray.Upper());
EXPECT_FALSE(anArray.IsEmpty());
EXPECT_TRUE(anArray.IsDeletable());
}
TEST(NCollection_Array1Test, SizeConstructor_ZeroSize)
{
NCollection_Array1<int> anArray(static_cast<size_t>(0));
EXPECT_EQ(0u, anArray.Size());
EXPECT_TRUE(anArray.IsEmpty());
}
TEST(NCollection_Array1Test, SizeConstructor_AtAccess)
{
const size_t aSize = 5;
NCollection_Array1<int> anArray(aSize);
for (size_t i = 0; i < aSize; ++i)
{
anArray.ChangeAt(i) = static_cast<int>(i * 10);
}
for (size_t i = 0; i < aSize; ++i)
{
EXPECT_EQ(static_cast<int>(i * 10), anArray.At(i));
}
}
TEST(NCollection_Array1Test, SizeConstructor_IteratorAccess)
{
const size_t aSize = 6;
NCollection_Array1<int> anArray(aSize);
int aFill = 0;
for (auto& aVal : anArray)
{
aVal = aFill++;
}
int aCheck = 0;
for (const auto& aVal : anArray)
{
EXPECT_EQ(aCheck++, aVal);
}
EXPECT_EQ(static_cast<int>(aSize), aCheck);
}
TEST(NCollection_Array1Test, SizeConstructor_LegacyValueOperator)
{
// When lower==0, operator[](i) should also work 0-based
const size_t aSize = 4;
NCollection_Array1<int> anArray(aSize);
for (size_t i = 0; i < aSize; ++i)
{
anArray[static_cast<int>(i)] = static_cast<int>(i + 100);
}
for (size_t i = 0; i < aSize; ++i)
{
EXPECT_EQ(static_cast<int>(i + 100), anArray[static_cast<int>(i)]);
}
}
TEST(NCollection_Array1Test, SizeConstructor_BufferReuse_NotOwner)
{
int aBuf[8] = {10, 20, 30, 40, 50, 60, 70, 80};
NCollection_Array1<int> anArray(aBuf, 8);
EXPECT_FALSE(anArray.IsDeletable());
EXPECT_EQ(8u, anArray.Size());
EXPECT_EQ(0, anArray.Lower());
EXPECT_EQ(7, anArray.Upper());
}
TEST(NCollection_Array1Test, SizeConstructor_BufferReuse_DataPreserved)
{
int aBuf[5] = {1, 2, 3, 4, 5};
NCollection_Array1<int> anArray(aBuf, 5);
for (size_t i = 0; i < 5; ++i)
{
EXPECT_EQ(static_cast<int>(i + 1), anArray.At(i));
}
}
TEST(NCollection_Array1Test, SizeConstructor_BufferReuse_WritesGoToBuffer)
{
int aBuf[4] = {0, 0, 0, 0};
{
NCollection_Array1<int> anArray(aBuf, 4);
for (size_t i = 0; i < 4; ++i)
{
anArray.ChangeAt(i) = static_cast<int>(i + 7);
}
}
// After array goes out of scope, buffer should still hold written values
for (int i = 0; i < 4; ++i)
{
EXPECT_EQ(i + 7, aBuf[i]);
}
}
TEST(NCollection_Array1Test, SizeConstructor_Resize_Grow)
{
NCollection_Array1<int> anArray(static_cast<size_t>(3));
anArray.ChangeAt(0) = 10;
anArray.ChangeAt(1) = 20;
anArray.ChangeAt(2) = 30;
anArray.Resize(static_cast<size_t>(5), true);
EXPECT_EQ(5u, anArray.Size());
EXPECT_EQ(10, anArray.At(0));
EXPECT_EQ(20, anArray.At(1));
EXPECT_EQ(30, anArray.At(2));
}
TEST(NCollection_Array1Test, SizeConstructor_Resize_Shrink)
{
NCollection_Array1<int> anArray(static_cast<size_t>(5));
for (size_t i = 0; i < 5; ++i)
{
anArray.ChangeAt(i) = static_cast<int>(i);
}
anArray.Resize(static_cast<size_t>(3), true);
EXPECT_EQ(3u, anArray.Size());
EXPECT_EQ(0, anArray.At(0));
EXPECT_EQ(1, anArray.At(1));
EXPECT_EQ(2, anArray.At(2));
}
TEST(NCollection_Array1Test, SizeConstructor_Resize_NoData)
{
NCollection_Array1<int> anArray(static_cast<size_t>(4));
anArray.ChangeAt(0) = 99;
anArray.Resize(static_cast<size_t>(6), false);
EXPECT_EQ(6u, anArray.Size());
EXPECT_TRUE(anArray.IsDeletable());
}
TEST(NCollection_Array1Test, SizeConstructor_MoveSemantics)
{
NCollection_Array1<int> anSrc(static_cast<size_t>(3));
anSrc.ChangeAt(0) = 1;
anSrc.ChangeAt(1) = 2;
anSrc.ChangeAt(2) = 3;
NCollection_Array1<int> anDst(std::move(anSrc));
EXPECT_EQ(3u, anDst.Size());
EXPECT_EQ(1, anDst.At(0));
EXPECT_EQ(2, anDst.At(1));
EXPECT_EQ(3, anDst.At(2));
EXPECT_EQ(0u, anSrc.Size());
EXPECT_TRUE(anSrc.IsEmpty());
}
@@ -629,4 +629,133 @@ TEST(NCollection_Array2Test, ResizeWithTrim_GrowPreservesOldRegion)
EXPECT_EQ(99, anArray(aRow, aCol));
}
}
}
// ============================================================================
// Zero-based (size_t) construction mode tests
// ============================================================================
TEST(NCollection_Array2Test, SizeConstructor_AllocatesCorrectly)
{
const size_t aNbRows = 3;
const size_t aNbCols = 4;
NCollection_Array2<int> anArray(aNbRows, aNbCols);
EXPECT_EQ(aNbRows, static_cast<size_t>(anArray.NbRows()));
EXPECT_EQ(aNbCols, static_cast<size_t>(anArray.NbColumns()));
EXPECT_EQ(aNbRows * aNbCols, anArray.Size());
EXPECT_EQ(0, anArray.LowerRow());
EXPECT_EQ(0, anArray.LowerCol());
EXPECT_EQ(static_cast<int>(aNbRows) - 1, anArray.UpperRow());
EXPECT_EQ(static_cast<int>(aNbCols) - 1, anArray.UpperCol());
EXPECT_TRUE(anArray.IsDeletable());
}
TEST(NCollection_Array2Test, SizeConstructor_AtAccess)
{
const size_t aNbRows = 3;
const size_t aNbCols = 5;
NCollection_Array2<int> anArray(aNbRows, aNbCols);
for (size_t aRow = 0; aRow < aNbRows; ++aRow)
{
for (size_t aCol = 0; aCol < aNbCols; ++aCol)
{
anArray.ChangeAt(aRow, aCol) = static_cast<int>(aRow * 100 + aCol);
}
}
for (size_t aRow = 0; aRow < aNbRows; ++aRow)
{
for (size_t aCol = 0; aCol < aNbCols; ++aCol)
{
EXPECT_EQ(static_cast<int>(aRow * 100 + aCol), anArray.At(aRow, aCol));
}
}
}
TEST(NCollection_Array2Test, SizeConstructor_LegacyOperatorZeroBased)
{
// When LowerRow/Col == 0, operator()(row, col) is also 0-based
NCollection_Array2<int> anArray(static_cast<size_t>(2), static_cast<size_t>(3));
anArray(0, 0) = 1;
anArray(0, 1) = 2;
anArray(1, 2) = 9;
EXPECT_EQ(1, anArray.At(0, 0));
EXPECT_EQ(2, anArray.At(0, 1));
EXPECT_EQ(9, anArray.At(1, 2));
}
TEST(NCollection_Array2Test, SizeConstructor_BufferReuse_NotOwner)
{
int aBuf[6] = {1, 2, 3, 4, 5, 6};
NCollection_Array2<int> anArray(aBuf, 2, 3);
EXPECT_FALSE(anArray.IsDeletable());
EXPECT_EQ(2, anArray.NbRows());
EXPECT_EQ(3, anArray.NbColumns());
EXPECT_EQ(0, anArray.LowerRow());
EXPECT_EQ(0, anArray.LowerCol());
}
TEST(NCollection_Array2Test, SizeConstructor_BufferReuse_DataPreserved)
{
// Row-major layout: buf[row * nbCols + col]
int aBuf[6] = {10, 20, 30, 40, 50, 60};
NCollection_Array2<int> anArray(aBuf, 2, 3);
EXPECT_EQ(10, anArray.At(0, 0));
EXPECT_EQ(20, anArray.At(0, 1));
EXPECT_EQ(30, anArray.At(0, 2));
EXPECT_EQ(40, anArray.At(1, 0));
EXPECT_EQ(50, anArray.At(1, 1));
EXPECT_EQ(60, anArray.At(1, 2));
}
TEST(NCollection_Array2Test, SizeConstructor_Resize_Grow)
{
NCollection_Array2<int> anArray(static_cast<size_t>(2), static_cast<size_t>(2));
anArray.Init(7);
anArray.Resize(static_cast<size_t>(3), static_cast<size_t>(3), true);
EXPECT_EQ(3, anArray.NbRows());
EXPECT_EQ(3, anArray.NbColumns());
// Original 2x2 values (linear copy) should be preserved in first 4 elements
EXPECT_EQ(7, anArray.At(0, 0));
EXPECT_EQ(7, anArray.At(0, 1));
}
TEST(NCollection_Array2Test, SizeConstructor_Resize_NoData)
{
NCollection_Array2<int> anArray(static_cast<size_t>(3), static_cast<size_t>(4));
anArray.Init(5);
anArray.Resize(static_cast<size_t>(2), static_cast<size_t>(2), false);
EXPECT_EQ(2, anArray.NbRows());
EXPECT_EQ(2, anArray.NbColumns());
EXPECT_EQ(0, anArray.LowerRow());
EXPECT_EQ(0, anArray.LowerCol());
EXPECT_TRUE(anArray.IsDeletable());
}
TEST(NCollection_Array2Test, SizeConstructor_ResizeWithTrim_GrowPreserves)
{
NCollection_Array2<int> anArray(static_cast<size_t>(2), static_cast<size_t>(2));
anArray.Init(42);
anArray.ResizeWithTrim(static_cast<size_t>(3), static_cast<size_t>(3), true);
EXPECT_EQ(3, anArray.NbRows());
EXPECT_EQ(3, anArray.NbColumns());
// Original 2x2 corner should be preserved
for (size_t aRow = 0; aRow < 2; ++aRow)
{
for (size_t aCol = 0; aCol < 2; ++aCol)
{
EXPECT_EQ(42, anArray.At(aRow, aCol));
}
}
}
@@ -49,6 +49,19 @@
//! @code
//! for (i = A.Lower(); i <= A.Upper(); i++)
//! @endcode
//!
//! Zero-based (size_t) construction mode:
//! Use NCollection_Array1(size_t theSize) or NCollection_Array1(pointer, size_t) to create
//! a zero-based array (Lower()==0). In this mode At()/ChangeAt() and STL iterators are the
//! preferred access path - they address elements directly without any offset subtraction.
//! Buffer-reuse variants do NOT own the memory and will not free it on destruction.
//! @code
//! int aBuffer[100];
//! NCollection_Array1<int> aZero(100); // allocates, lower=0
//! NCollection_Array1<int> aWrap(aBuffer, 100); // wraps aBuffer, lower=0, not owner
//! for (size_t i = 0; i < aWrap.Size(); ++i)
//! aWrap.At(i) = static_cast<int>(i);
//! @endcode
template <class TheItemType>
class NCollection_Array1
{
@@ -114,24 +127,6 @@ public:
construct(0, mySize);
}
explicit NCollection_Array1(const allocator_type& theAlloc,
const int theLower,
const int theUpper)
: myLowerBound(theLower),
mySize(theUpper - theLower + 1),
myPointer(nullptr),
myIsOwner(false),
myAllocator(theAlloc)
{
if (mySize == 0)
{
return;
}
myPointer = myAllocator.allocate(mySize);
myIsOwner = true;
construct(0, mySize);
}
explicit NCollection_Array1(const_reference theBegin,
const int theLower,
const int theUpper,
@@ -150,6 +145,32 @@ public:
construct(0, mySize);
}
//! Zero-based constructor: allocates theSize elements with lower bound 0.
//! Use At()/ChangeAt() or STL iterators for optimal access (no offset subtraction).
explicit NCollection_Array1(const size_t theSize)
: myLowerBound(0),
mySize(theSize)
{
if (mySize == 0)
{
return;
}
myPointer = myAllocator.allocate(mySize);
myIsOwner = true;
construct(0, mySize);
}
//! Zero-based buffer-reuse constructor: wraps an existing C array of theSize elements.
//! The array does NOT own the buffer and will NOT free it on destruction.
//! Use At()/ChangeAt() or STL iterators for optimal access (no offset subtraction).
explicit NCollection_Array1(pointer theBegin, const size_t theSize)
: myLowerBound(0),
mySize(theSize),
myPointer(theBegin),
myIsOwner(false)
{
}
//! Copy constructor
NCollection_Array1(const NCollection_Array1& theOther)
: myLowerBound(theOther.myLowerBound),
@@ -364,44 +385,15 @@ public:
void Resize(const int theLower, const int theUpper, const bool theToCopyData)
{
Standard_RangeError_Raise_if(theUpper < theLower, "NCollection_Array1::Resize");
const size_t aNewSize = static_cast<size_t>(theUpper - theLower + 1);
pointer aPrevContPnt = myPointer;
if (aNewSize == mySize)
{
myLowerBound = theLower;
return;
}
if (myIsOwner)
{
if (theToCopyData)
destroy(myPointer, aNewSize, mySize);
else
destroy(myPointer, 0, mySize);
}
myLowerBound = theLower;
if (theToCopyData)
{
const size_t aMinSize = (std::min)(aNewSize, mySize);
if (myIsOwner)
{
myPointer = myAllocator.reallocate(myPointer, aNewSize);
}
else
{
myPointer = myAllocator.allocate(aNewSize);
copyConstruct(aPrevContPnt, aMinSize);
}
construct(mySize, aNewSize);
}
else
{
if (myIsOwner)
myAllocator.deallocate(aPrevContPnt, mySize);
myPointer = myAllocator.allocate(aNewSize);
construct(0, aNewSize);
}
mySize = aNewSize;
myIsOwner = true;
resizeImpl(static_cast<size_t>(theUpper - theLower + 1), theLower, theToCopyData);
}
//! Resizes the array to theSize elements, keeping the lower bound unchanged.
//! @param theSize new number of elements
//! @param theToCopyData flag to copy existing data into new array
void Resize(const size_t theSize, const bool theToCopyData)
{
resizeImpl(theSize, myLowerBound, theToCopyData);
}
bool IsDeletable() const noexcept { return myIsOwner; }
@@ -409,6 +401,52 @@ public:
friend iterator;
friend const_iterator;
protected:
//! Core resize implementation used by all public Resize() overloads.
//! @param theNewSize new number of elements
//! @param theNewLower new lower bound value to store
//! @param theToCopyData whether to preserve existing elements
void resizeImpl(const size_t theNewSize, const int theNewLower, const bool theToCopyData)
{
const pointer aPrevPtr = myPointer;
if (theNewSize == mySize)
{
myLowerBound = theNewLower;
return;
}
if (myIsOwner)
{
if (theToCopyData)
destroy(myPointer, theNewSize, mySize);
else
destroy(myPointer, 0, mySize);
}
myLowerBound = theNewLower;
if (theToCopyData)
{
const size_t aMinSize = (std::min)(theNewSize, mySize);
if (myIsOwner)
{
myPointer = myAllocator.reallocate(myPointer, theNewSize);
}
else
{
myPointer = myAllocator.allocate(theNewSize);
copyConstruct(aPrevPtr, aMinSize);
}
construct(mySize < theNewSize ? mySize : theNewSize, theNewSize);
}
else
{
if (myIsOwner)
myAllocator.deallocate(aPrevPtr, mySize);
myPointer = myAllocator.allocate(theNewSize);
construct(0, theNewSize);
}
mySize = theNewSize;
myIsOwner = true;
}
protected:
const_reference at(const size_t theIndex) const
{
@@ -422,7 +460,6 @@ protected:
return myPointer[theIndex];
}
protected:
template <typename U = TheItemType>
typename std::enable_if<std::is_trivially_default_constructible<U>::value, void>::type construct(
const size_t,
@@ -39,6 +39,13 @@
*
* for (i = A.LowerRow(); i <= A.UpperRow(); i++)
* for (j = A.LowerCol(); j <= A.UpperCol(); j++)
*
* Zero-based (size_t) construction mode:
* NCollection_Array2(size_t theNbRows, size_t theNbCols) creates a zero-based array
* (LowerRow()==0, LowerCol()==0). In this mode At()/ChangeAt() and STL iterators are
* the preferred access path -- they address elements directly without any offset subtraction.
* Buffer-reuse variant NCollection_Array2(pointer, size_t, size_t) wraps an existing
* flat row-major buffer and does NOT own the memory.
*/
template <class TheItemType>
class NCollection_Array2 : public NCollection_Array1<TheItemType>
@@ -111,23 +118,6 @@ public:
{
}
//! Constructor
explicit NCollection_Array2(const allocator_type& theAlloc,
const int theRowLower,
const int theRowUpper,
const int theColLower,
const int theColUpper)
: NCollection_Array1<TheItemType>(
theAlloc,
BeginPosition(theRowLower, theRowUpper, theColLower, theColUpper),
LastPosition(theRowLower, theRowUpper, theColLower, theColUpper)),
myLowerRow(theRowLower),
mySizeRow(theRowUpper - theRowLower + 1),
myLowerCol(theColLower),
mySizeCol(theColUpper - theColLower + 1)
{
}
//! Copy constructor
NCollection_Array2(const NCollection_Array2& theOther)
: NCollection_Array1<TheItemType>(theOther),
@@ -169,6 +159,29 @@ public:
{
}
//! Zero-based constructor: allocates theNbRows x theNbCols elements with lower bounds 0.
//! Use At()/ChangeAt() or STL iterators for optimal access (no offset subtraction).
explicit NCollection_Array2(const size_t theNbRows, const size_t theNbCols)
: NCollection_Array1<TheItemType>(theNbRows * theNbCols),
myLowerRow(0),
mySizeRow(theNbRows),
myLowerCol(0),
mySizeCol(theNbCols)
{
}
//! Zero-based buffer-reuse constructor: wraps an existing flat row-major C array.
//! The array does NOT own the buffer and will NOT free it on destruction.
//! Use At()/ChangeAt() or STL iterators for optimal access (no offset subtraction).
explicit NCollection_Array2(pointer theBegin, const size_t theNbRows, const size_t theNbCols)
: NCollection_Array1<TheItemType>(theBegin, theNbRows * theNbCols),
myLowerRow(0),
mySizeRow(theNbRows),
myLowerCol(0),
mySizeCol(theNbCols)
{
}
//! Size (number of items).
size_t Size() const noexcept { return mySizeRow * mySizeCol; }
@@ -387,6 +400,34 @@ public:
resizeImpl<true>(theRowLower, theRowUpper, theColLower, theColUpper);
}
//! Zero-based Resize: resizes to theNbRows x theNbCols, keeping lower bounds unchanged.
//! No re-allocation is done if dimensions are unchanged.
//! @param theNbRows new number of rows
//! @param theNbCols new number of columns
//! @param theToCopyData flag to copy existing data into new array
void Resize(const size_t theNbRows, const size_t theNbCols, const bool theToCopyData)
{
Resize(myLowerRow,
myLowerRow + static_cast<int>(theNbRows) - 1,
myLowerCol,
myLowerCol + static_cast<int>(theNbCols) - 1,
theToCopyData);
}
//! Zero-based ResizeWithTrim: resizes preserving 2D layout, keeping lower bounds unchanged.
//! No re-allocation is done if dimensions are unchanged.
//! @param theNbRows new number of rows
//! @param theNbCols new number of columns
//! @param theToCopyData flag to copy existing data into new array
void ResizeWithTrim(const size_t theNbRows, const size_t theNbCols, const bool theToCopyData)
{
ResizeWithTrim(myLowerRow,
myLowerRow + static_cast<int>(theNbRows) - 1,
myLowerCol,
myLowerCol + static_cast<int>(theNbCols) - 1,
theToCopyData);
}
protected:
//! Resize without copying data.
void resizeNoData(int theRowLower, int theRowUpper, int theColLower, int theColUpper)