3

I have a project that wants me to make a BigNum class in c++ (university project) and it said to overload operator bracket for get and set but the problem is if the set was invalid we should throw an exception the invalid is like

BigNum a;
a[i]=11;//it is invalid because its >9

in searching I found out how to make the set work

C++ : Overload bracket operators [] to get and set

but I didn't find out how to manage setting operation in c# you easily can manage the set value what is the equivalent of it in c++

to make it clear in C# we can say

public int this[int key]
{
    set
    {
        if(value<0||value>9)throw new Exception();
        SetValue(key,value);
    }
}
Community
  • 1
  • 1
M.sadraei
  • 43
  • 1
  • 5
  • 1
    So you should only set or get individual digits? Then either you have to live with possible overflows, or you invent a wrapper class of some kind that is returned by the `operator[]` function, and whose `operator=` function validates the value. – Some programmer dude Mar 23 '17 at 11:50
  • 1
    What is it you don't understand about the answer you linked to? Post the code you're having problems with. – molbdnilo Mar 23 '17 at 11:52
  • I have no problem in that answer @molbdnilo i want to throw an exception if the set data is invalid invalid data is one digit can not be negative or higher than 10 – M.sadraei Mar 23 '17 at 11:55
  • sorry bro but I am new in english – M.sadraei Mar 23 '17 at 11:56
  • @M.sadraei So the problem is how to detect the invalid value and throw an exception? – molbdnilo Mar 23 '17 at 12:00
  • @M.sadraei Implementing such a wrapper isn't easy. Another workaround is to pass the rhs to a member function, say `void set(size_t index, int value)`. – felix Mar 23 '17 at 12:05
  • @felix the project asks me to do in that way I knew it is easy to define a function for it – M.sadraei Mar 23 '17 at 12:13
  • @molbdnilo yeah that's the thing I am looking for – M.sadraei Mar 23 '17 at 12:14

3 Answers3

5

New Answer

I have to rewrite my answer, my old answer is a disaster.

The check should happen during the assignment, when the right hand side (11) is available. So the operator which you need to overload is operator=. For overloading operator=, at least one of its operands must be an user defined type. In this case, the only choice is the left hand side.

The left hand side we have here is the expression a[i]. The type of this expression, a.k.a the return type of operator[], must be an user defined type, say BigNumberElement. Then we can declare an operator= for BigNumberElement and do the range check inside the body of operator=.

class BigNum {
public:
  class BigNumberElement {
  public:
    BigNumberElement &operator=(int rhs) {
      // TODO : range check
      val_ = rhs;
      return *this;
    }

  private:
    int val_ = 0;
  };

  BigNumberElement &operator[](size_t index) {
    return element_[index];
  }

  BigNumberElement element_[10];
};

OLD answer

You can define a wapper, say NumWapper, which wraps a reference of BigNum's element. The operator= of BigNum returns the wrapper by value.

a[i]=11;

is then something like NumWrapper x(...); x = 11. Now you can do those checks in the operator= of NumWrapper.


class BigNum {
 public:
  NumWrapper operator[](size_t index) {
    return NumWrapper(array_[index]);
  }

  int operator[](size_t index) const {
    return array_[index];
  }
};

In the NumWrapper, overload some operators, such as:

class NumWrapper {
 public:
  NumWrapper(int &x) : ref_(x) {}
  NumWrapper(const NumWrapper &other) : ref_(other.ref_) {}

  NumWrapper &operator=(const NumWrapper &other);
  int operator=(int x);
  operator int();

 private:
  int &ref_;
};

You can also declare the NumWrapper's copy and move constructor as private, and make BigNum his friend, for preventing user code from copying your wrapper. Such code auto x = a[i] will not compile if you do so, while user code can still copy the wrapped value by auto x = static_cast<T>(a[i]) (kind of verbose though).

auto &x = a[i]; // not compiling
const auto &x = a[i]; // dangerous anyway, can't prevent.

Seems we are good.


These is also another approach: store the elements as a user defined class, say BigNumberElement. We now define the class BigNum as :

class BigNum {
 // some code
 private:
  BigNumberElement array_[10];
}

We need to declare a whole set operators for BigNumberElement, such as comparison(can also be done through conversion), assignment, constructor etc. for making it easy to use.

auto x = a[i] will now get a copy of BigNumberElement, which is fine for most cases. Only assigning to it will sometimes throw an exception and introduce some run-time overhead. But we can still write auto x = static_cast<T>(a[i]) (still verbose though...). And as far as I can see, unexpected compile-time error messages is better than unexpected run-time exceptions.

We can also make BigNumberElement non-copyable/moveable... but then it would be the same as the first approach. (If any member functions returns BigNumberElement &, the unexpected run-time exceptions comes back.)

felix
  • 2,213
  • 7
  • 16
  • 1
    Wouldn't it be cleaner to just store the elements as NumWrapper in the first place? I can imagine all kinds of issues with templated code that tries to copy an element and instead ends up with a reference (the NumWrapper) to the original. – josefx Mar 23 '17 at 12:38
  • @josefx I did a quick analyse for the approach you proposed. Copying NumWrapper is still a bad idea. – felix Mar 23 '17 at 13:30
1

the following defines a type foo::setter which is returned from operator[] and overloads its operator= to assign a value, but throws if the value is not in the allowed range.

class foo
{
  int data[10];
public:
  void set(int index, int value)
  {
    if(value<0 || value>9)
      throw std::runtime_error("foo::set(): value "+std::to_string(value)+" is not valid");
    if(index<0 || index>9)
      throw std::runtime_error("foo::set(): index "+std::to_string(index)+" is not valid");
    data[index] = value;
  }
  struct setter {
    foo &obj;
    size_t index;
    setter&operator=(int value)
    {
      obj.set(index,value);
      return*this;
    }
    setter(foo&o, int i)
    : obj(o), index(i) {}
  };
  int operator[](int index) const // getter
  { return data[index]; }
  setter operator[](int index) // setter
  { return {*this,index}; }
};
Walter
  • 44,150
  • 20
  • 113
  • 196
0

If what you are trying to do is overload [] where you can input info like a dict or map like dict[key] = val. The answer is actually pretty simple:

lets say you want to load a std::string as the key, and std::vector as the value. and lets say you have an unordered_map as your underlying structure that you're trying to pass info to

std::unordered_map<std::string, std::vector<double>> myMap;

Inside your own class, you have this definition:

class MyClass{
    private:
        std::unordered_map<std::string, std::vector<double>> myMap;
    public:
        std::vector<double>& operator [] (std::string key) {
        return myMap[key];
    }

}

Now, when you want to load your object, you can simply do this:

int main() {

    std::vector<double> x;
    x.push_back(10.0);
    x.push_back(20.0);
    x.push_back(30.0);
    x.push_back(40.0);
    
    MyClass myClass;
    myClass["hello world"] = x;

    double x = myClass["hello world"][0]; //returns 10.0
}

The overloaded [] returns a reference to where that vector is stored. So, when you call it the first time, it returns the address of where your vector will be stored after assigning it with = x. The second call returns the same address, now returning the vector you had input.

Adam Boyle
  • 99
  • 1
  • 5