Sign in to follow this  
Rattrap

Fighting with overloading the [] operator in C++

Recommended Posts

Rattrap    3385
I've been writing a small wrapper class that uses the stl vector inside of it. I'm working on overloading the [] operator. I was under the impression that there were 2 different versions of it that I should implement. One for reads and one for writes. I'm at work, so I don't have access to my C++ reference books at home, but I thought the fuctions were some where along the lines of type operator [] (const int i) const; // Read type& operator [] (const int i); // Write THe wrapper class I'm writing has a read only property to it, and I want to restrict changing a value if the flag is set. The problem I get is both member functions compile fine in MSVC 2005. But at runtime, on both read and write operations the "type& operator [] (const int i)" version is called. There really isn't a good way I can think of to implement the write protection in a single function, since it has to pass by reference and doesn't really have any control over what is happening to the variable at that point. Am I just misunderstanding the concept or just getting some bit of syntax wrong
//  Very simple example
class List
{
public:
     List() : m_bReadOnly(true)
     {
     }

int operator [] (const int i) const  // Read
{
     return m_List[i];
}
int& operator [] (const int i);  // Write
{
     if(m_bReadOnly)
     {
          throw 1;
     }

     return m_List[i];
}

private:
std::vector<int> m_List;
bool m_bReadOnly;
};

Share this post


Link to post
Share on other sites
Guest Anonymous Poster   
Guest Anonymous Poster
The const version is only ever called when invoked from a const function or a const instance or reference to your class. All other invocations will use the non-const version.

Share this post


Link to post
Share on other sites
Sneftel    1788
And it doesn't look like that. Both return references, and neither take const ints (there is no reason for function arguments to be const types, ever). The prototypes are as follows:

type const& operator [] (size_t i) const; // Read-only
type & operator [] (size_t i); // Read-write

Share this post


Link to post
Share on other sites
Xai    1848
Quote:
Original post by Sneftel
And it doesn't look like that. Both return references, and neither take const ints (there is no reason for function arguments to be const types, ever). The prototypes are as follows:

type const& operator [] (size_t i) const; // Read-only
type & operator [] (size_t i); // Read-write


editing your statement:

there is no reason for non-reference function arguments to be const types

Share this post


Link to post
Share on other sites
Xai    1848
Also, if you need to provide run-time write protection to a value you cannot use these functions, because there is no way to hook into a write into the int& you return to block it.

You will have to go to some form of setter function.

Share this post


Link to post
Share on other sites
arithma    226
Quote:
Original post by Xai
Quote:
Original post by Sneftel
And it doesn't look like that. Both return references, and neither take const ints (there is no reason for function arguments to be const types, ever). The prototypes are as follows:

type const& operator [] (size_t i) const; // Read-only
type & operator [] (size_t i); // Read-write


editing your statement:

there is no reason for non-reference function arguments to be const types


Actually references are const by nature.. You meant that there are reasons for references to be referencing const data.
You probably already know that, but someone else reading the post might get confused.

Share this post


Link to post
Share on other sites
arithma    226
Quote:
Original post by arithma
Quote:
Original post by Xai
Quote:
Original post by Sneftel
And it doesn't look like that. Both return references, and neither take const ints (there is no reason for function arguments to be const types, ever). The prototypes are as follows:

type const& operator [] (size_t i) const; // Read-only
type & operator [] (size_t i); // Read-write


editing your statement:

there is no reason for non-reference function arguments to be const types


Actually references are const by nature.. You meant that there are reasons for references to be referencing const data.
You probably already know that, but someone else reading the post might get confused.


And now I see that you weren't talking about references themselves.

Share this post


Link to post
Share on other sites
Sneftel    1788
Quote:
Original post by Xai
there is no reason for non-reference function arguments to be const types

There is ESPECIALLY no reason for reference function arguments to have const type, since there is no reason for references themselves to be const (see here). It is often useful, however, for references to be references to const types.

Share this post


Link to post
Share on other sites
Agony    3452
Quote:
Original post by Rattrap
The wrapper class I'm writing has a read only property to it, and I want to restrict changing a value if the flag is set. The problem I get is both member functions compile fine in MSVC 2005. But at runtime, on both read and write operations the "type& operator [] (const int i)" version is called. There really isn't a good way I can think of to implement the write protection in a single function, since it has to pass by reference and doesn't really have any control over what is happening to the variable at that point. Am I just misunderstanding the concept or just getting some bit of syntax wrong

That sounds very much like the .NET collections, and the way they do what they do is through runtime polymorphism. You'd have to have virtual setters of some form.

I personally detest .NET's collection types. To make a read only version of a custom collection, you have to write a new class that wraps the read/write version, or build in checks into your original class all over the place. Additionally, you have to use the polymorphic behavior, which while not horrible, would be nice to avoid when possible. Another problem is that all write attempts on read-only collections are only caught at run-time, never compile-time.

I find the const-correctness idea of C++ to be preferable. (And when you really do need runtime behavior, you can still program it it; it's just not available in the standard collections).

Share this post


Link to post
Share on other sites
Rattrap    3385
Part of this is to try and emulate some of the .NET collections. It may end up being scrapped if I really hate the way it ends up / doesn't really accomplish what I want it to.

THe sample was a very stripped down version of the code. The real version uses a fairly complicated template setup. I moved the bool to a template paramater and added a little code using the Loki libraries that made the type stored in the array automatically a const type. So the value could be read, but should cause a compile time error. I'm not sure this is ultimately going to geive me what I really want, but I'm giving it a shot.

Again, thanks for the responses.

Share this post


Link to post
Share on other sites
iMalc    2466
Quote:
Original post by Sneftel
And it doesn't look like that. Both return references, and neither take const ints (there is no reason for function arguments to be const types, ever). The prototypes are as follows:

type const& operator [] (size_t i) const; // Read-only
type & operator [] (size_t i); // Read-write
That's a bit closed-minded, or oversimplified.
You don't have to return a reference for the const version, in particular if the return type is no bigger than the size of an int anyway, then returning by value is not going to be any slower, in fact possibly faster.

"no reason" is also too cut and dry for me: It can be handy for the implementor of the function to make sure they don't modify the passed in parameters. I know it's a very weak reason, but it is still a reason.

Share this post


Link to post
Share on other sites
Sneftel    1788
Quote:
Original post by iMalc
You don't have to return a reference for the const version, in particular if the return type is no bigger than the size of an int anyway, then returning by value is not going to be any slower, in fact possibly faster.

You don't have to, but the high amount of work you'd need to do to get that sort of specialization probably isn't worth the very slim chance that it would ever improve the speed. (On a decent compiler with inlining, both would very likely generate the same code for primitive types.)

Quote:
"no reason" is also too cut and dry for me: It can be handy for the implementor of the function to make sure they don't modify the passed in parameters. I know it's a very weak reason, but it is still a reason.

If you need that, simply make a reference to const within the function, and use that reference instead of the function argument. That way, you don't need to show off the constness to the users of the interface, who don't care about it.

Share this post


Link to post
Share on other sites
Nitage    1107
Quote:
Original post by Sneftel
Quote:
Original post by iMalc
You don't have to return a reference for the const version, in particular if the return type is no bigger than the size of an int anyway, then returning by value is not going to be any slower, in fact possibly faster.

You don't have to, but the high amount of work you'd need to do to get that sort of specialization probably isn't worth the very slim chance that it would ever improve the speed.


It's not hard work at all - use the boost call_traits library. It provides a type which acts like a const reference on large types and a return by value on small types.

Quote:

On a decent compiler with inlining, both would very likely generate the same code for primitive types.

A compiler may be able to do this in a simple case, but in a more complex case the compiler would have to prove that the reference is never const_casted AND that the container never alters the object referenced before the returned const reference drops out of scope. This requires the mythical compiler that can solve the halting problem.

Share this post


Link to post
Share on other sites
Sneftel    1788
Quote:
Original post by Nitage
Quote:
Original post by Sneftel
Quote:
Original post by iMalc
You don't have to return a reference for the const version, in particular if the return type is no bigger than the size of an int anyway, then returning by value is not going to be any slower, in fact possibly faster.

You don't have to, but the high amount of work you'd need to do to get that sort of specialization probably isn't worth the very slim chance that it would ever improve the speed.


It's not hard work at all - use the boost call_traits library. It provides a type which acts like a const reference on large types and a return by value on small types.
Nifty. I didn't know it included that.

Quote:
Quote:

On a decent compiler with inlining, both would very likely generate the same code for primitive types.

A compiler may be able to do this in a simple case, but in a more complex case the compiler would have to prove that the reference is never const_casted AND that the container never alters the object referenced before the returned const reference drops out of scope. This requires the mythical compiler that can solve the halting problem.

The only case where it wouldn't be simple would be one where the returned value were actually stored or passed around as a const reference--not unheard-of, but certainly not the most common case. More commonly, you just test it or do arithmetic with it.

Share this post


Link to post
Share on other sites
iMalc    2466
Quote:
Original post by Sneftel
Quote:
Original post by Nitage
Quote:
Original post by Sneftel
Quote:
Original post by iMalc
You don't have to return a reference for the const version, in particular if the return type is no bigger than the size of an int anyway, then returning by value is not going to be any slower, in fact possibly faster.

You don't have to, but the high amount of work you'd need to do to get that sort of specialization probably isn't worth the very slim chance that it would ever improve the speed.


It's not hard work at all - use the boost call_traits library. It provides a type which acts like a const reference on large types and a return by value on small types.
Nifty. I didn't know it included that.
Yeah, cool!

Actually, to be pedantic, isn't the parameter of the [] operator supposed to be ptrdiff_t, not size_t?

Share this post


Link to post
Share on other sites
Sneftel    1788
You'd think so. I am utterly at a loss as to why std::vector::operator[] takes a size_t.

Share this post


Link to post
Share on other sites
Xai    1848
Quote:
Original post by Sneftel
Quote:
Original post by Nitage
Quote:
Original post by Sneftel
Quote:
Original post by iMalc
You don't have to return a reference for the const version, in particular if the return type is no bigger than the size of an int anyway, then returning by value is not going to be any slower, in fact possibly faster.

You don't have to, but the high amount of work you'd need to do to get that sort of specialization probably isn't worth the very slim chance that it would ever improve the speed.


It's not hard work at all - use the boost call_traits library. It provides a type which acts like a const reference on large types and a return by value on small types.
Nifty. I didn't know it included that.

Quote:
Quote:

On a decent compiler with inlining, both would very likely generate the same code for primitive types.

A compiler may be able to do this in a simple case, but in a more complex case the compiler would have to prove that the reference is never const_casted AND that the container never alters the object referenced before the returned const reference drops out of scope. This requires the mythical compiler that can solve the halting problem.

The only case where it wouldn't be simple would be one where the returned value were actually stored or passed around as a const reference--not unheard-of, but certainly not the most common case. More commonly, you just test it or do arithmetic with it.


no, the case where the compiler cannot turn them into the same code is the case where the compiler chooses not to inline the function - so ANY function which is called multiple times and is significant in size will generate function call code, which will NOT turn a reference into a non-reference, period.

Share this post


Link to post
Share on other sites
Sneftel    1788
Quote:
Original post by Xai
no, the case where the compiler cannot turn them into the same code is the case where the compiler chooses not to inline the function - so ANY function which is called multiple times and is significant in size will generate function call code, which will NOT turn a reference into a non-reference, period.

Yes, that's why I said "with inlining".

Share this post


Link to post
Share on other sites
Nitage    1107
Quote:
Original post by Sneftel
You'd think so. I am utterly at a loss as to why std::vector::operator[] takes a size_t.

Quote:

Actually, to be pedantic, isn't the parameter of the [] operator supposed to be ptrdiff_t, not size_t?


My copy of the standard says that std::vector<T>::operator[] should take a std::vector<T>::size_type, and that std::vector<T>::size_type is implentation defined. Seeing as ptrdiff_t is a signed integral type and it doesn't make any sense to have a container with a 0-based index that uses negative indexes, the vast majority of std library implemetors who use size_t made the right call IMO.

Share this post


Link to post
Share on other sites
Xai    1848
Quote:
Original post by Sneftel
Quote:
Original post by Xai
no, the case where the compiler cannot turn them into the same code is the case where the compiler chooses not to inline the function - so ANY function which is called multiple times and is significant in size will generate function call code, which will NOT turn a reference into a non-reference, period.

Yes, that's why I said "with inlining".


I took your initial quote to mean ... If the compiler supports inlining (like any good one does) then they will be the same. Not, for the narrow set of simple cases that the compiler generates inlined code, they will be the same. So what I was pointing out was that - on the best of compilers, they cannot be the same except in the cases where inlining is the most efficient choice. (in others words, very small functions).

But, I agree with the earlier ideas that worrying about such a silly little "possible" performance issue is completely silly until after a profiler shows it to be a bottleneck in some real world program.

Hell, almost all the code I write starts out as general templates first (using const & and &, never value semantics) - and only later do I write the value versions and specializations if they are worth it.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this