0

I was making my own bound-checking array class, but I got a problem that it is unable to call the appropriate overloaded function when using the operator [].

The first overloaded operator [] is for getting an actual value(=content) of the array instance: such an instructionnum1 = list1[2] to be possible.

The second overloaded operator [] is for assigning the given rvalue to the MyArrayList.

But when running main function, it was unable to call the second one. the whole code is like this:

#include <iostream>

template <typename Data>
class MyArrayList {
    private :
        Data* data_storage; 
        int num_of_elements;
        int array_size;
        int idx_cursor;
    public :
        MyArrayList(int _size);
        void add_element(Data new_data);
        void remove_element();
        Data operator[](int idx) const;
        MyArrayList& operator[](int idx);
        void operator=(Data new_data);
        ~MyArrayList();
};

template <typename Data>
MyArrayList<Data>::MyArrayList(int _size) {
    array_size = _size;
    data_storage = new Data[array_size];
    num_of_elements = 0;
    idx_cursor = 0;
}

template <typename Data>
void MyArrayList<Data>::add_element(Data new_data) {
    if(num_of_elements > array_size) {
        std::cout << "Unable to store more." << std::endl;
        return; 
    }
    data_storage[++num_of_elements - 1] = new_data;
}

template <typename Data>
void MyArrayList<Data>::remove_element() {
    if(num_of_elements <= 0) {
        std::cout << "Unable to delete anymore" << std::endl; 
        return;
    }
    
    Data* temp = data_storage;
    delete[] data_storage;
    
    data_storage = new Data[--num_of_elements];  
    for(int i = 0; i < num_of_elements; i++)
        data_storage[i] = temp[i];
    
    delete[] temp; 
}

template <typename Data>
MyArrayList<Data>::~MyArrayList() {
    delete[] data_storage;
}

template <typename Data>
Data MyArrayList<Data>::operator[](int idx) const { //var = list1[5];
    if(idx < 0) {
        int new_idx = idx;
        while(new_idx < 0) {
        std::cout << "IndexOutofBounds! Enter new index." << std::endl;
        std::cin >> new_idx; std::cout << std::endl;
        new_idx--;
        }
        idx = new_idx;
    }
    
    return data_storage[idx];
}

template <typename Data>
 MyArrayList<Data>& MyArrayList<Data>::operator[](int idx){ // list1[2] = 5;
    idx_cursor = idx;
    return *this;

}

template <typename Data>
void MyArrayList<Data>::operator=(Data new_data){
    data_storage[idx_cursor] = new_data;        
} 

int main() {
    int num1;
    MyArrayList<int> list1(5);
    
    list1.add_element(6);
    list1.add_element(7);
    list1.add_element(8);
    list1.add_element(9);
    list1[2] = 5; //possible
    //std::cout << num1 << std::endl; //not possible. probably not calling the first overloaded operator[]()? 
    
}

I first tried to rewrite the second overloaded operator[]() with using friend keyword, but in the second guess, I thought it was not a good idea, and there's no way coming up with to solve the problem.

  • You could try simplyfing for facilitating the debugging. Did you succeed to do that with a non-template version, e.g. simple `int`? – Yunnosch Mar 29 '23 at 10:46
  • @ragedhumancompiler num1 was not assigned a value. – Vlad from Moscow Mar 29 '23 at 10:46
  • `list1` is not a `const` object. That is why both the `add_element()` member function and the non-`const` version `operator[]()` can be called. Roughly speaking, the only circumstances in which the `const` version of `operator[]()` will be called is if the object is `const` or if a `const` reference to it is obtained . For example, adding `const MyArrayList &ref(list1); num1 = ref[2];` to your `main()` would call the `const` version of `operator[]()`, and a subsequent `ref[2] = 5` would be a diagnosable error. – Peter Mar 29 '23 at 10:56
  • Aside from the misunderstanding of what the `const` qualifier does or does not, the shown logic of doing bounds checking, but then if it's out of bounds then a message gets printed on `cout` and a new index is read from `cin` -- this goes against the grain, somewhat, and I would not expect that. The expected behavior is what `std::vector::at` does: throw an exception. – Sam Varshavchik Mar 29 '23 at 10:58
  • You realize your assignment-based overload design is inherently non-thread-safe and non-reentrant, right? And by using `operator=` as part of it, you make it *impossible* to bulk-reassign your container? I'd strongly recommend reading [the rules and idioms for operator overloading](https://stackoverflow.com/a/4421719/364696); there's a reason they exist, and you're violating several of them. – ShadowRanger Mar 29 '23 at 10:58
  • @Yunnosch I'll do that shortly. A bit of tidy-up needed to make it clearer, I think, which is why I commented first (to give a pointer on what the OP's not understanding). – Peter Mar 29 '23 at 13:31

2 Answers2

0

It is possible to call it, but you have to tell compiler that you want your object as const:

std::cout << const_cast<const MyArrayList<int>&>(list1)[2] << std::endl; 

But your approach is going to bring you some Very Angry Users. You are breaking about every reasonable expectation users may have on your class. Your assignment operator doesn't return assigned object, it allows assigning from contained type (and doesn't allow assigning actual different container) and it relies on internal state that is for some reason set via operator[].

The expected implementation should be as follows:

const Data& MyArrayList<Data>::operator[](int idx) const 
{
    //implement actual accessing here
}

Data& MyArrayList<Data>::operator[](int idx)
{
    // looks scary, but it's valid way to avoid code duplication
    // We first cast it to const to call const version of operator, then we remove const from the result
    // It's safe because we know this isn't const, so its members also aren't const
    return const_cast<Data&>(const_cast<const MyArrayList<Data>*>(this)->operator[](idx));

    //it's not impossible to just copy implementation from const operator here, but one should avoid duplicating code
}

// copy assignment operator. Don't forget the other functions required for the Rule of Five!
MyArrayList<Data>& MyArrayList<Data>::operator=(const MyArrayList<Data>&)
{
    // implement copying from another object here
    return *this;
}
Yksisarvinen
  • 18,008
  • 2
  • 24
  • 52
  • Actually, not knowing C++ operators&template rules, it was quite difficult for me to find out the internal running sequences of the C++ grammars. By the way, thanks for your advice.. – ragedhumancompiler Mar 29 '23 at 14:33
0

The basic concern here is that you're not understanding what happens when a class MyArrayList has overloaded operator[](), and has provide both a const and a non-const version. (The fact your class is templated doesn't change things in this case).

Data operator[](int idx) const;
MyArrayList& operator[](int idx);

Note: I'm not addressing the fact that your const and non-const overloads have different return types (for a MyArrayList<int>, the const overload of operator[]() returns int and the non-const overload returns MyArrayList<int> &). In short - don't do that!

At the call site, given an object list1 of type MyArrayList<int> and an expression list1[2], both of the operator[]() are candidates for being called.

When both a const and a non-const overload are available, the decision on which to call is (roughly speaking) based on whether the object list1 is const or not.

In your code, list1 is not const. That is the reason main() can call list1.add_element(6) (and other calls of add_element()). It is also why the non-const overload of operator[]() is called.

In the expression list1[2] = 5, list1[2] is evaluated, as a complete sub-expression in its own right. The = 5 has no bearing on which overload of operator[]() is called, and certainly does not force selection of an operator[]() (your const overload that returns an int &).

Roughly speaking, the only circumstances in which the const version of operator[]() will be called is if the object is const or if the object is accessed via a const reference.

For example, if you add the following two lines to your main()

const MyArrayList<int> &ref(list1);
ref[2];        

then the const overload of operator()[] would be called.

Another example would be to create a function

void func(const MyArrayList<int> &object)
{
   object[2];
}

which would also call the same const overload.

Peter
  • 35,646
  • 4
  • 32
  • 74