I have a custom template class Array<T>
, and am currently implementing the assignment operator.
However, I've come across a design decision: When assigning two arrays, must I require them to have the same size, or adjust the LHS Array
size to the RHS?
Pros and Cons of Requiring Equal Size
Pro
- Most of the time, the array is often thought of as a static container (there are
std::vector
, etc. for dynamic containers). - It's more efficient, because I do not have to reallocate; rather, I overwrite each element of the LHS.
Con
- It's a hassle for the user to do the work himself if he does intend to have a variable-length
Array
, plus it would be inefficient.
Code
Here are the implementations of the two versions I'm considering.
Equal Size
template <typename T>
bool JKArray<T>::operator=(const JKArray<T> & rhs)
{
if (this == *rhs) { return true; }
else
{
if (size_ != rhs.size_) { return false; }
else
{
for (int i = 0; i < size_; i++) { array_[i] = rhs.array_[i]; }
}
}
}
Unequal Size
template <typename T>
void JKArray<T>::operator=(const JKArray<T> & rhs)
{
if (this == *rhs) { }
else
{
if (size_ != rhs.size_)
{
delete [] array_;
size_ = rhs.size_;
array_ = new T[size_];
}
for (int i = 0; i < size_; i++) { array_[i] = rhs.array_[i]; }
}
}
Note
Regarding the implementation of Equal Size: I return false
if the size is not equal, and true
otherwise. Of course, I could use exceptions, however this is satisfactory for now.
I'm leaning towards making operator=
fail* if RHS is a different length than LHS.
I'd like to know what other's have done, and what the prefer. To avoid closing this on the basis of opinion-based, please give explanations as to why one might be preferred over the other.
*See 'Note' under 'Code'.
1 Answer 1
template <typename T>
bool JKArray<T>::operator=(const JKArray<T> & rhs)
template <typename T>
void JKArray<T>::operator=(const JKArray<T> & rhs)
Neither of them have the correct signature for a copy assignment. The correct signature for that operator is class_name & class_name :: operator= ( const class_name & )
.
That already rules out abusing the return value to signal success or failure. If you want runtime errors, you got to do it with exceptions.
If you violate that signature, it makes common patterns such as chained assignment (a = b = c = {...};
) impossible. This may break other templated libraries depending on your datatypes to adhere to the standards.
Especially returning a bool
may cause rather unexpected side effects.
Pro
- Most of the time, the array is often thought of as a static container (there are
std::vector
, etc. for dynamic containers).- It's more efficient, because I do not have to reallocate; rather, I overwrite each element of the LHS.
Con
- It's a hassle for the user to do the work himself if he does intend to have a variable-length
Array
, plus it would be inefficient.
That list is by no means complete. There are more aspects to cover:
Iterator invalidation
When you have to re-allocate the backing data structure, it would be trivial to keep all existing iterators valid for same-size copies, while the same feat is problematic for different sizes.
Heap vs stack allocation
For fixed size arrays, it's not even necessary to allocate them on the heap, sufficiently small arrays may as well be allocated straight on the stack. This is obviously impossible to do with the dynamically sized ones.
Array or Vector?
While you call it
Array
, if you make it re-sizable it behaves more like anstd::vector
rather than anstd::array
.Size as part of the type?
As @Olzhas mentioned, it's an option to include the size of the backing array as a template parameter. This provides compile time checks for compatible or incompatible sizes. Whereby compile time checks - if applicable - are obviously to be preferred.
This would actually be how
std::array
does it.
-
\$\begingroup\$ Very comprehensive answer; thanks, especially for mentioning the signature; I was aware of the unconventional return type because I'm not a fan of
a=b=c;
, but I didn't realize that it could affect other libraries. The reason of not implementing exceptions was that I haven't learned them yet (as mentioned previously, I'm learning C++), so I hacked a simple way to check the status of success. However, after reading your note on incorrect signatures, I'll juststd::cerr
instead (until I learn about exceptions). \$\endgroup\$Fine Man– Fine Man2016年11月01日 19:53:13 +00:00Commented Nov 1, 2016 at 19:53 -
\$\begingroup\$ Regarding "include the size of the backing array as a template parameter": I'm hesitant to implement this, because then I would not be able to implement
operator+
(or, at least, I'm not aware of any other ways to do so), unless the user sums the sizes when declaring their instances (but then it's a hassle to the user, which I'd like to avoid). \$\endgroup\$Fine Man– Fine Man2016年11月01日 20:18:20 +00:00Commented Nov 1, 2016 at 20:18 -
\$\begingroup\$ @SirJony Of course you would be able to implement
operator+
, at least as long that's supposed to mean element wise addition. If that's supposed to mean concatenation though, no, then it won't be possible. That requires to use dynamic sizes to begin with. You got to decide what you need, define the requirements. Unless you know them, neither the fixed nor the dynamic sized container are inherently better. \$\endgroup\$Ext3h– Ext3h2016年11月01日 20:26:15 +00:00Commented Nov 1, 2016 at 20:26 -
\$\begingroup\$ Yes, sorry. I meant concatenation. I've implemented the class with dynamic allocation. The discussion is heading off-topic (not your fault, but it's not related to the Q anymore), so in case I need further clarification on designing my class, I'll post another question. Thanks! \$\endgroup\$Fine Man– Fine Man2016年11月01日 20:29:29 +00:00Commented Nov 1, 2016 at 20:29
std::vector<>
. Otherwise it is not an array, it is surprising array at best. I'd expectsizeof(a)
to be the same as for C array, but in your code it will probably besizeof(T*)
. \$\endgroup\$