Skip to content

Commit 56b38d0

Browse files
committed
Fix leaks and self-assignment in Array2D
The copy and move assignment operators overwrote the data pointer without releasing the previous buffer, leaking it on every assignment to a non-empty array Copy self-assignment also allocated the new buffer before reading from the source, so the array was filled from its own uninitialised memory. Now guards against self-assignment and releases the old buffer.
1 parent 41fd1ea commit 56b38d0

2 files changed

Lines changed: 74 additions & 6 deletions

File tree

‎Framework/Core/include/Framework/Array2D.h‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,29 @@ struct Array2D {
7979

8080
Array2D& operator=(Array2D<T> const& other)
8181
{
82-
this->rows = other.rows;
83-
this->cols = other.cols;
84-
data = new T[rows * cols];
85-
for (auto i = 0U; i < rows; ++i) {
86-
for (auto j = 0U; j < cols; ++j) {
87-
data[i * cols + j] = *(other.data + (i * cols + j));
82+
if (this == &other) {
83+
return *this;
84+
}
85+
// Copy into a new buffer first, so that a throwing copy leaves this array untouched
86+
auto* newData = new T[other.rows * other.cols];
87+
for (auto i = 0U; i < other.rows; ++i) {
88+
for (auto j = 0U; j < other.cols; ++j) {
89+
newData[i * other.cols + j] = other.data[i * other.cols + j];
8890
}
8991
}
92+
delete[] data;
93+
data = newData;
94+
this->rows = other.rows;
95+
this->cols = other.cols;
9096
return *this;
9197
}
9298

9399
Array2D& operator=(Array2D<T>&& other)
94100
{
101+
if (this == &other) {
102+
return *this;
103+
}
104+
delete[] data;
95105
this->rows = other.rows;
96106
this->cols = other.cols;
97107
data = other.data;

‎Framework/Core/test/test_Variants.cxx‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,64 @@ TEST_CASE("Array2DTest")
229229
}
230230
}
231231

232+
TEST_CASE("Array2DAssignmentTest")
233+
{
234+
std::vector<int> v = {1, 2, 3, 4, 5, 6};
235+
Array2D<int> source(v, 2, 3);
236+
237+
// copy assignment to an array of different shape makes an independent deep copy
238+
Array2D<int> copied(std::vector<int>{7}, 1, 1);
239+
copied = source;
240+
REQUIRE(copied.rows == 2);
241+
REQUIRE(copied.cols == 3);
242+
REQUIRE(copied.data != source.data);
243+
source[0][0] = 42;
244+
REQUIRE(copied(0, 0) == 1);
245+
for (auto i = 1U; i < 6; ++i) {
246+
REQUIRE(copied(i / 3, i % 3) == v[i]);
247+
}
248+
249+
// self copy assignment keeps the contents
250+
auto& self = copied;
251+
copied = self;
252+
REQUIRE(copied.rows == 2);
253+
REQUIRE(copied.cols == 3);
254+
REQUIRE(copied(1, 2) == 6);
255+
256+
// move assignment takes over the buffer and empties the source
257+
auto* buffer = copied.data;
258+
Array2D<int> moved(std::vector<int>{7, 8}, 1, 2);
259+
moved = std::move(copied);
260+
REQUIRE(moved.data == buffer);
261+
REQUIRE(moved.rows == 2);
262+
REQUIRE(moved.cols == 3);
263+
REQUIRE(moved(1, 2) == 6);
264+
REQUIRE(copied.data == nullptr);
265+
REQUIRE(copied.rows == 0);
266+
REQUIRE(copied.cols == 0);
267+
268+
// self move assignment keeps the contents
269+
auto& selfMoved = moved;
270+
moved = std::move(selfMoved);
271+
REQUIRE(moved.data == buffer);
272+
REQUIRE(moved(1, 2) == 6);
273+
274+
// assigning an empty array releases the old buffer and leaves an empty array
275+
moved = Array2D<int>{};
276+
REQUIRE(moved.data == nullptr);
277+
REQUIRE(moved.rows == 0);
278+
REQUIRE(moved.cols == 0);
279+
280+
// strings are copied element by element
281+
std::vector<std::string> s = {"one", "two"};
282+
Array2D<std::string> ms(s, 2, 1);
283+
Array2D<std::string> msc;
284+
msc = ms;
285+
ms[0][0] = "changed";
286+
REQUIRE(msc(0, 0) == "one");
287+
REQUIRE(msc(1, 0) == "two");
288+
}
289+
232290
TEST_CASE("LabeledArrayTest")
233291
{
234292
float m[3][4] = {{0.1, 0.2, 0.3, 0.4}, {0.5, 0.6, 0.7, 0.8}, {0.9, 1.0, 1.1, 1.2}};

0 commit comments

Comments
 (0)