Reputation: 3001
I'm porting some Fortran90 code to C++ (because I'm stupid, to save the "Why?!").
Fortran allows specification of ranges on arrays, in particular, starting at negative values, eg
double precision :: NameOfArray(FirstSize, -3:3)
I can write this in C++ as something like
std::array<std::array<double, 7>, FirstSize> NameOfArray;
but now I have to index like NameOfArray[0:FirstSize-1][0:6]
. If I want to index using the Fortran style index, I can write perhaps
template <typename T, size_t N, int start>
class customArray
{
public:
T& operator[](const int idx) { return data_[idx+start]; }
private:
std::array<T,N> data_;
}
and then
customArray<double, 7, -3> NameOfArray;
NameOfArray[-3] = 5.2;
NameOfArray[3] = 2.5;
NameOfArray[4] = 3.14; // This is out of bounds,
// despite being a std::array of 7 elements
So - the general idea is "Don't inherit from std::'container class here'". My understanding is that this is because, for example, std::vector does not have a virtual destructor, and so should not (can not?) be used polymorphically.
Is there some other way I can use a std::array
, std::vector
, etc, and get their functions 'for free', whilst overriding specific functions?
template<typename T, size_t N>
T& std::array<T,N>::operator[](const int idx) { ... };
might allow me to override the operator, but it won't give me access to knowledge about a custom start point - making it completely pointless. Additionally, if I were to optimistically think all my customArray
objects would have the same offset, I could hardcode that value - but then my std::array is broken (I think).
How can I get around this? (Ignoring the simple answer - don't - just write myArray[idx-3]
as needed)
Upvotes: 3
Views: 1070
Reputation: 223
It doesn't seem that bad to just stick with composition, and write wrappers for the member functions you need. There aren't that many. I'd even be tempted to make the array
data member public so you can access it directly when needed, although some people would consider that a bigger no-no than inheriting from a base class without a virtual destructor.
template <typename T, size_t N, int start>
class customArray
{
public:
std::array<T,N> data;
T& operator[](int idx) { return data[idx+start]; }
auto begin() { return data.begin(); }
auto begin() const { return data.begin(); }
auto end() { return data.end(); }
auto end() const { return data.end(); }
auto size() const { return data.size(); }
};
int main() {
customArray<int, 7, -3> a;
a.data.fill(5); // can go through the `data` member...
for (int& i : a) // ...or the wrapper functions (begin/end).
cout << i << endl;
}
Upvotes: 0
Reputation: 73490
General thought
The recommendation not to inherit from standard vector, is because this kind of construct is often misunderstood, and some people are tempted to make all kind of objects inherit from a vector, just for minor convenience.
But this rule should'nt become a dogma. Especially if your goal is to make a vector class, and if you know what you're doing.
Danger 1: inconsistency
If you have a very important codebase working with vectors in the range 1..size instead of 0..size-1, you could opt for keeping it according to this logic, in order not to add thousands of -1 to indexes, +1 to index displayed, and +1 for sizes.
A valid approach could be to use something like:
template <class T>
class vectorone : public vector<T> {
public:
T& operator[] (typename vector<T>::size_type n) { return vector<T>::operator[] (n-1); }
const T& operator[] (typename vector<T>::size_type n) const { return vector<T>::operator[] (n-1); }
};
But you have to remain consitent accross all the vector interface :
const T& operator[]()
. If youd don't overload it, you'll end up having wrong behaviour if you have vectors in constant objects. at()
which shall be consitent with []
So you have free functionality, but there's more work ahead than initially thougt. The option of creating your own object with a more limited interface, and a private vector could in the end be a safer approach.
Danger 2:more inconsistency
The vector indexes are vector<T>::size_type
. Unfortunately this type is unsigned. The impact of inherit from vector, but redefine operator[]
with signed integer indexes has to be carefully analysed. This can lead to subtle bugs according to the way the indexes are defined.
Conclusions:
There's perhap's more work that you think to offer a consistent std::vector
interface. So in the end, having your own class using a private vector could be the safer approach.
You should also consider that your code will be maintained one day by people without fortran background, and they might have wrong assumptions about the []
in your code. Is going native c++ really out of question ?
Upvotes: 2
Reputation: 27153
so should not (can not?) be used polymorphically.
Don't give up too soon. There are basically two issues to consider with inheritance in C++.
Such objects, derived classes with non-virtual destructors in the base, can be used safely in a polymorphic fashion, if you basically follow one simple rule: don't use delete
anywhere. This naturally means that you cannot use new
. You generally should be avoiding new
and raw pointers in modern C++ anyway. shared_ptr
will do the right thing, i.e. safely call the correct destructor, as long as you use make_shared
:
std:: shared_ptr<Base> bp = std:: make_shared<Derived>( /* constructor args */ );
The type parameter to make_shared
, in this case Derived
, not only controls which type is created. It also controls which destructor is called. (Because the underlying shared-pointer object will store an appropriate deleter.)
It's tempting to use unique_ptr
, but unfortunately (by default) it will lead to the wrong deleter being used (i.e. it will naively use delete
directly on the base pointer). It's unfortunate that, alongside the default unique_ptr
, there isn't a much-safer-but-less-efficient unique_ptr_with_nice_deleter
built into the standard.
Even if std::array
did have a virtual destructor, this current design would still be very weird. Because operator[]
is not virtual, then casting from customArray*
to std:: array*
would lead to the wrong operator[]
. This isn't really a C++-specific issue, it's basically the issue that you shouldn't pretend that customArray
isa std:: array
.
Instead, just decide that customArray
is a separate type. This means you couldn't pass an customArray*
to a function expecting std::array*
- but are you sure you even want that anyway?
Is there some other way I can use a std::array, std::vector, etc, and get their functions 'for free', whilst overloading specific functions?
This is a good question. You do not want your new type to satisfy isa std::array
. You just want it to behave very similar to it. As if you magically copied-and-pasted all the code from std::array
to create a new type. And then you want to adjust some things.
Use private
inheritance, and using
clauses to bring in the code you want:
template <typename T, size_t N, int start>
struct customArray : private std::array<T,N>
{
// first, some functions to 'copy-and-paste' as-is
using std::array<T,N> :: front;
using std::array<T,N> :: begin;
// finally, the functions you wish to modify
T& operator[](const int idx) { return data_[idx+start]; }
}
The private
inheritance will block conversions from customArray *
to std::array *
, and that's what we want.
PS: I have very little experience with private
inheritance like this. So many it's not the best solution - any feedback appreciated.
Upvotes: 2
Reputation: 14392
A range converter class could be the solution although you would need to make it yourself, but it would allow you to get the range size to initialize the vector and to do the conversion.
Untested code:
struct RangeConv // [start,end[
{
int start, end;
RangeConv(int s, int e) : start(s), end(e) { }
int size() const { return end - start; }
int operator()(int i) { return i - start; } // possibly check whether in range
}
RangeConv r(-3, 3);
std::vector<int> v(r.size());
v[r(-3)] = 5;
Upvotes: 4
Reputation: 1901
There's no problem with inheriting standard containers. This is only generally discouraged because this imposes several limitations and such an inheritance is not the way how inheritance was originally predicted in C++ to be used. If you are careful and aware of these limitations, you can safely use inheritance here.
You just need to remember that this is not subclassing and what this really means. In particular, you shouldn't use pointers or references to the object of this class. The problem might be if you pass a value of MyVector<x>*
where vector<x>*
was expected. You should also never create such objects as dynamic (using new
), and therefore also delete
these objects through the pointer to the base class - simply because destructor call will not forward to your class's destructor, as it's not virtual.
There's no possibility to prevent casting of the "derived pointer" to the "base pointer", but you can prevent taking a pointer from an object by overloading the &
operator. You can also prevent creating objects of this class dynamically by declaring an in-class operator new
in private section (or = delete
should work as well).
Don't also think about private inheritance. This is merely like containing this thing as a field in private section, except the accessor name.
Upvotes: 6