简体   繁体   中英

Overloading const version of [] operator for a templated class

Yet another question on overloading an [] operator in C++, in particular, its const version.

According to cppreference page on operator overloading , when overloading an array subscript operator

struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };

If the value type is known to be a built-in type, the const variant should return by value.

So, if value_t happens to be a built-in type , the const variant should look

    const value_t operator[](std::size_t idx) const { return mVector[idx]; }

or probably even

   value_t operator[](std::size_t idx) const { return mVector[idx]; }

since the const qualifier is not very useful on such a return value.


Now, I have a templated class T (to keep the same naming as with the reference), which is used both with built-in data types and user-defined ones, some of which might be heavy.

template<class VT>
struct T
{
          VT& operator[](std::size_t idx)       { return mVector[idx]; }
    const VT& operator[](std::size_t idx) const { return mVector[idx]; }
};

According to the given advice above, I should use enable_if with some type_traits to distinguish between templated class instantiations with built-in/not built-in types.

Do I have to do it? Is this recommendation only to avoid potential unnecessary dereferencing for built-in types or something else is hiding behind it that one should be aware of?


Notes:

  • this class actively participates in a hot part of the code instantiated both with built-in and custom types.
  • code is used cross-platform with multiple compilers with varying degrees of optimization options.
  • thus, I am interested in making it both correct & portable, as well as avoid any potential detriment to performance.
  • I was not able to find any additional reasoning in C++ standard, but reading standardeze is not my strongest suit.

Existing questions on StackOverflow:

I don't agree with the above "advice". Consider this:

T t = /*Initialize `t`*/;
const T::value_t &vr = std::as_const(t)[0];
const auto test = vr; //Copy the value
t[0] = /*some value other than the original one.*/
assert(test != vr);

Does the assert trigger? It shouldn't trigger, because we are just referencing the value in the container. Basically, std::as_const(t)[i] should have the same effect as std::as_const(t[i]) . But it doesn't if your const version returns a value. So making such a change fundamentally changes the semantics of the code.

So even if you know value_t is a fundamental type, you should still return a const& .

Note that C++20 ranges officially recognize ranges which do not return actual value_type& s from their operator* or equivalent functions. But even then, such things are a fundamental part of the nature of that range, rather than being a property that changes based on the template parameter (see vector<bool> for reasons why this is a bad idea).

You do not need to handle fundamental types in a special way. Simply always return value_t& for the non- const variant and const value_t& for the const variant.

The overloads are typically short, as in your examples, so they will be inlined at every call site anyway. In that case it doesn't matter whether the overload returns by-value or by-reference, in either case the indirection will be optimized out. Any somewhat modern compiler set to at least a low optimization level should do that.

There isn't any other reason to handle fundamental types differently that I can think of either.


I would also caution that if you are eg implementing a container class as in your examples, returning a reference and returning a value will have different semantics to the user.

If you return a const reference to an element, the user can keep that reference around and observe changes in the container's element (until reference invalidation happens in some specified manner). If you return a value, it is impossible to observe changes.

It would be surprising to a user if they are able to obtain a reference and observe future changes for some types, but not for others. Worst case, if they use type-generic code as well, they would also need to condition all their templates.

Also, even if you return by-value for fundamental types, the overload returning by-value will only be called through const references to the object. In most cases the user probably has a non- const instance of your container and to make use of this potential optimization, they would need to explicitly cast their object reference to const first before calling the operator overload.

So if optimization is a concern I would rather add an additional member function which always returns a copy of the container element by-value and which can be used by the user if the potential dereferencing is identified as performance issue. Calling this member function won't be any more trouble than making sure to call the correct operator overload.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM