Free code review.
Here's a simplified version of your code
replacing goto
with loops (this revolution in language development happened in 1970, get with the times!)
use the right names! Naming is important.
In particular
- capacity means "limit to potential size" (what you names "size" or "arraySize" before)
- size means "count of elements in the collection" (e.g. size grows on insertion)
- (capacity - size) is free space/room/available capacity. This is what you named "capacity" before
Violating expectations of identifier meanings leads to bugs. In fact, you had that, because your own test sequence used getData(4)
on a holder that contained 0 elements (_size == 0
).
Same for operations. insert(...)
should insert, not overwrite (see below), resize(...)
should change the size
, not the capacity
.
To change the capacity, the standard library uses the term reserve(...)
, which I also used for consistency and least-surprise.
Also, names should be descriptive. In a strong typed language naming the variable after its type is redundant information. Also, you can names variables strA
, strB
, strC
but it's more informative to name them givenName
, middleNames
, surName
. Note that suddenly we have semantic information (is it lastName
, surName
, birthName
, familyName
. So much information is conveyed).
In this sense, I renamed ptr
to _data
While we're at it, we might rename the class to be descriptive: DynaArray<T>
tells you more than dynamicDataHolder<T>
(that could be std::optional<T>
, std::unique_ptr<T>
, std::vector<T>
, harddiskVolume
etc).
As a minor footnote it is a good idea to have a naming convention for (private) member variables, so you will know what variables are local and which are member. I used _data
for _ptr
You had a number of places (2 if I remember correctly) where you had off-by-one boundary checks (think pos > capacity
instead of >=
).
Separation Of Concerns. dynamicDataHolder<T>
is a data holder, not an interactiveUserInterviewAboutErrorConditions<T>
or databaseUpdateUserInterface<T>
. So, don't do UI in the class. Change void insertAtNthPos()
to void setData(size_t pos, T value)
.
Again, naming is important. Don't promise to insert
if really you don't unless it's beyond current size bounds. "Insert 7 at pos 1 in {1,2,3}" should result in "{1,7,2,3}", not "{1,7,3}". The name setData
- does what is expected and
- separates UI concerns and
- nicely mirrors
getData(size_t pos)
Oh, use consistent index types (size_t in my version) so that you avoid funny looking checks (pos > 0
?) and bugs due to mixed signed/unsigned comparisons.
Yes, you had them. No you wouldn't have caught them because you didn't have compiler warnings enabled. Let this reinforce the mantra: "Use The Force Diagnostics"
Don't do "using namespace" at global scope. Or not at all (Why is "using namespace std;" considered bad practice?)
DRY. DRY. DRY. (Don't Repeat Yourself). Sooo many examples of this. You had at least 3 copy-element-data loops. You had several places that resize the allocated array. You had funny stuff like:
void insert(t data) {
int pos;
if (elements >= size) {
int updatedSize = (2 * size);
t* tempPtr = ptr;
ptr = new t[updatedSize];
for (int i = 0; i < size; ++i) {
ptr[i] = tempPtr[i];
}
delete[] tempPtr;
tempPtr = NULL;
size = updatedSize;
pos = elements;
ptr[pos] = data;
incrementElement();
}
else {
if (elements == 0) {
pos = 0;
ptr[pos] = data;
incrementElement();
}
else {
pos = elements;
ptr[pos] = data;
incrementElement();
}
}
It should be obvious that this does the exact same thing three times. The if (elements == 0)
check is completely useless, because if you don't have the check, the else
branch does the same anyways. For comparison, the new insert
looks like:
void insert(T data) { setData(_size, data); }
Oh oops, that got completely zapped, because really, there is nothing special about inserting. But setData
is also only ~4 lines of code.
when you add items you have to be careful: you had a very helpful "capacity check". And if the capacity was not enough you doubled capacity. HOWEVER. Who said that was enough? You need to either keep doubling until space is adequate:
void setData(size_t pos, T value) {
while (pos >= _capacity) {
grow();
}
_size = std::max(_size, pos + 1);
_data[pos] = value;
}
or reserve sufficient size at once:
size_t required = _capacity;
while (pos >= required)
required *= 2;
reserve(required);
Const-Correctness: all the observers should be marked const
so the compiler knows what it can optimize and the user has guarantees what will never change:
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
size_t available() const { return _capacity - _size; }
bool compare(const DynArray<T>& obj) const;
The repeated copy/initialization loops can be fixed by using the copy constructor. In related context, make the constructor use a base/member initializer list:
explicit DynArray(size_t initialCapacity = 5)
: _capacity(initialCapacity),
_data(initialCapacity != 0u ? new T[_capacity]{} : nullptr) {}
Now the copy constructor can be:
DynArray(DynArray const& obj) : DynArray(obj.capacity()) {
_size = obj.size();
for (size_t i = 0; i < capacity(); ++i)
_data[i] = obj._data[i];
}
That's the last time you will ever see a copy loop. Why? Observe:
How would you reserve
more / less space and without manually copying items? Wouldn't you need a dynamic array class of some kind?
Oh wait. You're implementing it! Don't forget the code you already have:
void reserve(size_t updatedSize) {
DynArray tmp(updatedSize);
for (size_t i = 0; i < std::min(capacity(), tmp.capacity()); ++i) {
tmp._data[i] = _data[i];
}
*this = std::move(tmp);
}
void grow() { reserve(2 * _capacity); }
Now the real magic is in the move constructor/assignment operations.
Copy-and-Swap idiom to the rescue. Turns out you can implement these operations really efficiently and generally with little code:
DynArray(DynArray&& obj) : DynArray(0) { swap(obj); }
DynArray& operator=(DynArray rhs) {
swap(rhs);
return *this;
}
See. Simplicity is a driver. The key is that you just swap with a temporary. The temporary gets destructed, as you want. The swap is pretty simple in its own right:
void swap(DynArray& rhs) {
assert(rhs._data);
std::swap(_capacity, rhs._capacity);
std::swap(_size, rhs._size);
std::swap(_data, rhs._data);
}
There's a lot more thinking behind Copy-And-Swap, but you can find that on your own if you really want: What is the copy-and-swap idiom?
compare checked capacities. I'd say that from a logical standpoint that shouldn't matter.
DynArray<int> a(20), b(12);
for (int v : {1,2,3}) { a.insert(v); b.insert(v); }
assert(a == b); // should pass
it also had bugs, a spurious
result = false;
and erroneous
if (ptr[i] == obj.ptr[i])
{
result = true;
}
That made no sense. If a single item matches, doesn't mean all the previous mismatches are gone.
The above has removed a lot of code, and many many lurking bugs or maintenance head-aches. I reworded your test code to be ... readable. The gotos are obviously gone, the main is now "sane":
int main() {
std::cout << "What do you want to do?\n"
<< "\t1. Run Test Sequence\n"
<< "\t0. Exit Program\n";
for (int userChoice = 0; std::cin >> userChoice;) {
switch (userChoice) {
case 1: testSequence(); break;
case 0: return 0;
default: std::cout << "\nInvalid selection - please try again.\n";
}
}
}
And the information displayed is more complete and much less error prone. Naming can still be improved (obj2
? obj4
?!??) but I had no semantic information to go by, so I left that as an exorcism for the reader, and also so that you will have an easy time cross-referencing your own test sequence code with mine.
Live Demo
Live On Coliru
#include <iostream>
#include <stdexcept>
#include <cassert>
template <class T> class DynArray {
size_t _capacity = 0;
size_t _size = 0;
T* _data = nullptr;
public:
explicit DynArray(size_t initialCapacity = 5)
: _capacity(initialCapacity),
_data(initialCapacity != 0u ? new T[_capacity]{} : nullptr) {}
DynArray(DynArray const& obj) : DynArray(obj.capacity()) {
_size = obj.size();
for (size_t i = 0; i < capacity(); ++i)
_data[i] = obj._data[i];
}
~DynArray() { delete[] _data; }
void swap(DynArray& rhs) {
assert(rhs._data);
std::swap(_capacity, rhs._capacity);
std::swap(_size, rhs._size);
std::swap(_data, rhs._data);
}
DynArray(DynArray&& obj) : DynArray(0) { swap(obj); }
DynArray& operator=(DynArray rhs) {
swap(rhs);
return *this;
}
T getData(size_t pos) const {
if (pos >= _size) {
throw std::out_of_range("pos");
}
return _data[pos];
}
void setData(size_t pos, T value) {
while (pos >= _capacity)
grow();
_size = std::max(_size, pos+1);
_data[pos] = value;
}
void insert(T data) { setData(_size, data); }
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
size_t available() const { return _capacity - _size; }
bool compare(const DynArray& obj) const {
if (obj._size != _size) {
return false;
}
for (size_t i = 0; i < _size; i++) {
if (_data[i] != obj._data[i])
return false;
}
return true;
}
void reserve(size_t updatedSize) {
DynArray tmp(updatedSize);
for (size_t i = 0; i < std::min(capacity(), tmp.capacity()); ++i) {
tmp._data[i] = _data[i];
}
*this = std::move(tmp);
}
void grow() { reserve(2 * _capacity); }
};
template <typename T>
void dump(std::string caption, DynArray<T> const& da) {
std::cout << caption << " Capacity: " << da.capacity() << ","
<< " Size: " << da.size() << ","
<< " Available: " << da.available() << ","
<< " Data: {";
for (size_t i = 0; i < da.size(); i++)
std::cout << " " << static_cast<int>(da.getData(i));
std::cout << " }\n";
}
void testSequence() {
{
DynArray<int> obj;
for (size_t i = 0; i < 5; i++) {
obj.insert(i * 2);
}
dump("obj", obj);
if (15 < obj.capacity()) {
std::cout << "\nWarning! Entered size is less than the current size.\n"
<< "Loss of data may occur. Continue? (Y/N)\n";
for (char userChoice; std::cin >> userChoice;) {
if (userChoice == 'y' || userChoice == 'Y') {
obj.reserve(15);
break;
}
if (userChoice == 'n' || userChoice == 'N') {
break;
}
std::cout << "\nInvalid selection - please try again.\n";
}
}
DynArray<int> obj4;
obj4 = obj;
dump("obj4", obj);
}
DynArray<char> obj2(5);
DynArray<char> obj3 = obj2;
dump("obj2", obj2);
dump("obj3", obj3);
std::cout << "obj2 == obj3: " << std::boolalpha << obj3.compare(obj2) << "\n";
try {
std::cout << "obj2.getData(4): " << obj2.getData(4) << std::endl;
} catch(std::out_of_range const& e) {
std::cout << "out of range: " << e.what() << std::endl;
}
while (true) {
std::cout << "\nEnter the details below to insert obj2 values:-\n";
std::cout << "\nPosition: ";
long pos = 0;
if (std::cin >> pos) {
if (pos < 0)
break;
std::cout << "Value: ";
int value = 0;
if (std::cin >> value) {
obj2.setData(pos, value);
}
}
if (!std::cin.eof())
std::cin.clear();
else
break;
dump("obj2", obj2);
}
}
int main() {
do {
std::cout << "What do you want to do?\n"
<< "\t1. Run Test Sequence\n"
<< "\t0. Exit Program\n";
if (int userChoice = 0; std::cin >> userChoice) {
switch (userChoice) {
case 1: testSequence(); continue;
case 0: std::cout << "Goodbye\n"; return 0;
default: std::cout << "\nInvalid selection - please try again.\n";
}
}
if (!std::cin.eof())
std::cin.clear();
} while (std::cin.good());
}
When run with e.g.
g++ -std=c++17 -O2 -Wall -Wextra -pedantic -pthread -fsanitize=address,undefined main.cpp -o sotest
./sotest <<< "1 30 99 3 42 0 -1 0 8 -1 1 -1 0"
You will see the following output:
What do you want to do?
1. Run Test Sequence
0. Exit Program
obj Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj4 Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj2 Capacity: 5, Size: 0, Available: 5, Data: { }
obj3 Capacity: 5,
Size: 0, Available: 5, Data: { } obj2 == obj3: true
obj2.getData(4): out of range: pos
Enter the details below to insert obj2 values:-
Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }
Enter the details below to insert obj2 values:-
Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { 0
0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }
Enter the details below to insert obj2 values:-
Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { -1
0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }
Enter the details below to insert obj2 values:-
Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { 8
0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }
Enter the details below to insert obj2 values:-
Position: What do you want to do?
1. Run Test Sequence
0. Exit Program
obj Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj4 Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj2 Capacity: 5, Size: 0, Available: 5, Data: { }
obj3 Capacity: 5,
Size: 0, Available: 5, Data: { }
obj2 == obj3: true
obj2.getData(4): out of range: pos
Enter the details below to insert obj2 values:-
Position: What do you want to do?
1. Run Test Sequence
0. Exit Program
Goodbye