Usually, overloaded index operators return references:
// Return reference to some string contained in MyStrings object.
std::string &MyStrings::operator[](std::size_t idx);
Occasionally however, we need to provide a class with an index operator that cannot return a reference. This happens e.g. when there is nothing to reference in the first place, or when behavior of the return value must change depending on whether the index operator is used as an rvalue or as an lvalue. (Examples further down).
We then have to create some nested Proxy type and return that, but writing one is repetitive work, and somewhat error-prone.
The IndexProxifier CRTP template takes a derived class as its template type
parameter, and creates for that derived class the nested LRProxy class, as
well as the index operator(s) that return an LRProxy.
class MyClass : protected IndexProxifier < MyClass >
{...};
or
Class MyClass : protected IndexProxifier < MyClass,
my_keychoice_policy >
{...};
The IndexProxifier uses C++20 concepts, so -std=c++20 is required.
In order for IndexProxifier to work, the derived class (above: MyClass) must
do the following:
-
Provide a (private) function
[constexpr] ret_type proxy_return_action(Key key) const;The resulting ret_type will be returned by the index operator when it is on the right hand side of an assignment:
MyClass mc; ret_type const value = mc[some_key]; -
Provide a (private) function
[constexpr] some_type proxy_accept_action(Key key, Value value);This will be called when the index operator is used on the left hand side of an assignment. The return type some_type will be returned from the assignment operator on an LRProxy:
MyClass mc; mc[some_key] = mc[some_other_key] = some_value; -
Declare
friend class IndexProxifier<myclass>;so the proxifier will be allowed to call the above two functions.
-
Make the generated index operator(s) public, if desired:
public: using IndexProxifier<myclass>::operator[];
The template IndexProxifier uses the CRTP to provide its template parameter with:
LRProxy<Key, Derived> operator[](Key &&);
The LRProxy is sensitive to being being on the left hand side or right hand side of an assignment.
IndexProxifier has no members. Empty base class optimization applies. It doesn't have a virtual destructor. So insofar as public inheritance suggests polymorphism, it is unfit for that. Use private or protected inheritance instead.
IndexProxifier will handle situations where operator[] cannot return a value or reference.
Most index operators can and do return by value or by reference. E.g.
constexpr int &vector<int>::operator[](size_t index);
constexpr int const &vector<int>::operator[](size_t index) const;
Sometimes this is not possible, e.g. with
bitset::operator[](size_t index); // Assuming 8 bits stored in 1 byte.
A single bit is not addressable. Moreover, it matters whether the returned type is used as an lvalue or an rvalue:
bitset bs;
bool truth = bs[0]; // Just convert to bool.
bs[0] = true; // Flip bit in bs *after* operator[] has returned(!)
The solution is to return an LRProxy that holds a reference to the indexed
object as well as the index. The LRProxy has a conversion operator (in this
case to bool so truth can be set. And it has an assignment operator (from
bool here) so that bs[0] can be set.
IndexProxifier creates this LRProxy based on the proxy_return_action and
proxy_accept_action member functions.
IndexProxifier tries hard to Just Work.
-
A hand-crafted Proxy might create an undesirable back door:
struct myclass::proxy { myclass &owner; // Modifiable even if proxy is const }; myclass const mc; mc[1] = true; // Inadvertently allowed.
IndexProxifier prevents this by transferring the cv- and rvalue-reference qualifiers of the indexed object onto the owner inside the LRProxy.
-
Often, a hand-crafted myclass' operator const can return by value. IndexProxifier instead returns an LRProxy even here:
LRProxy operator[](keytype key) const;
It does this for multiple reasons:
-
Orthogonality, or the principle of least surprise. The non-const index operator will not call proxy_return_action upon creation of the LRProxy return object, but only when the LRProxy is converted to a value. Therefore, the const index operator should also only call proxy_return_action when conversion is done.
-
Odd configurations, e.g. proxy_accept_action(...) const, perhaps with a mutable data member, don't break anything.
-
If the user would rather have a
return_type operator[](...)const, that is easily hand-crafted with a wrapper:myclass::value_type myclass::operator[](keytype key) const { // perhaps even use (assuming conversion): return IndexProxifier::operator; }
-
The LRProxy deletes its copy constructor to prevent dangerous constructions like:
auto cpy = bitset{}[2]; cpy = true; // Temporary bitset already destructed.
and like:
auto function()
{
bitset bs;
return bs[3]; // 'bs' going out of scope.
}
Both are dangerous because binding a return value to a class-scope reference does not extend the lifetime of the object.
-
For the same reason, storing the
keyby value is to be preferred over storing it by reference. Yet if the key is an object, which may be expensive to copy, or ifproxy_accept_actionorproxy_return_action take the key by non-const reference, LRProxy must store it by reference. The user can overide this behavior by providing IndexProxifier with a second formal parameter that implements a key choice policy class. Such a non-default policy class can start out as a copy of the defaultPreferValuePreferConst, which is commented to explain. -
To prevent the idiom:
auto const &lref = bs[3]; // Bad: dangling danger.
the LRProxy's assignment operator works only if it is an rvalue reference.
The LRProxy should be a transparent proxy, never seen nor handled by the user. Yet the user can do e.g.:
auto &&bad_thing = myclass{}[1]; //rref to LRProxy
auto &&also_bad = myclass{}[1] = myclass{}[2];
// Temporary myclass objects already destructed.
bad_thing = true; // May segfault.
This binds rvalue references to anonymous temporary LRProxies. It extends the lifetime of the proxies, but those proxies hold internal references that are already dangling when used.
This bug is not unique to IndexProxifier. Hand-crafted proxies suffer from it as well. In fact, we mitigate it by returning a value from LRProxy::operator=, which is possible and makes sense because a copy of an LRProxy references the same owner as the original, and the copy will be relatively light-weight (unless the index type is expensive to copy). But it is still bad and ugly.
The class EightBits below, by deriving from IndexProxifier, creates an index operator that allows getting/setting individual bits.
class EightBits: protected IndexProxifier<EightBits>
{
uint8_t d_data = 0;
public:
// One for modifiable and one for const objects.
using IndexProxifier<EightBits>::operator[];
private:
// Let IndexProxifier access private functions.
friend IndexProxifier<EightBits>;
bool proxy_return_action(int key) const;
bool proxy_accept_action(int key, bool value);
... all other member functions left out ...
};
// When used as rvalue, convert to bool.
inline bool EightBits::proxy_return_action(int key) const
{
bool retval = (d_data & bitmask(key)) != 0;
return retval;
}
// When used as lvalue, use overloaded assignment to raise/lower one bit.
inline bool EightBits::proxy_accept_action(int key, bool value)
{
data_t mask = bitmask(key);
if (value)
d_data |= mask;
else
d_data &= ~mask;
return value;
}
int main()
{
EightBits eb;
// eb[0] on rhs is converted to bool
bool value = eb[0];
// eb[0] on lhs can be assigned to
eb[0] = 1;
// chaining works
eb[2] = eb[1] = eb[0];
EightBits const ebc;
value = ebc[1];
ebc[1] = false; // Won't compile: ebc[1] is const if ebc is const;
}
Another case is e.g. an Environment class, where
Environment env; // class definition not shown here
env["HOME"] = "/root"; // needs to call setenv
cout << env["HOME"]; // merely needs getenv
The LRProxy was written with const-correctness in mind.
When the compiler is allowed to optimize (-O2), typically no IndexProxifier or
even LRProxy objects show up in the object files or executables. Only the
proxy_accept_action and proxy_return_action function members of the
inheriting class will be called.