Reputation: 2326
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:
Existing questions on StackOverflow:
const value_t&
and the second const value_t
.Upvotes: 3
Views: 195
Reputation: 22152
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 e.g. 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.
Upvotes: 2
Reputation: 473302
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).
Upvotes: 4