Preface
I'm asking this question after a week of investigating and reviewing dozens and dozens of proxy pattern implementations.
Please, do not incorrectly flag this question as a duplicate unless the answer does not break (1) member access for structs & class types and (2) interaction with primitive types.
Code
For my Minimal, Reproducible Example I'm using code from @Pixelchemist as the base.
#include <vector>
#include <type_traits>
#include <iostream>
template <class T, class U = T, bool Constant = std::is_const<T>::value>
class myproxy
{
protected:
U& m_val;
myproxy& operator=(myproxy const&) = delete;
public:
myproxy(U & value) : m_val(value) { }
operator T & ()
{
std::cout << "Reading." << std::endl;
return m_val;
}
};
template <class T>
struct myproxy < T, T, false > : public myproxy<T const, T>
{
typedef myproxy<T const, T> base_t;
public:
myproxy(T & value) : base_t(value) { }
myproxy& operator= (T const &rhs)
{
std::cout << "Writing." << std::endl;
this->m_val = rhs;
return *this;
}
};
template<class T>
struct mycontainer
{
std::vector<T> my_v;
myproxy<T> operator[] (typename std::vector<T>::size_type const i)
{
return myproxy<T>(my_v[i]);
}
myproxy<T const> operator[] (typename std::vector<T>::size_type const i) const
{
return myproxy<T const>(my_v[i]);
}
};
int main()
{
mycontainer<double> test;
mycontainer<double> const & test2(test);
test.my_v.push_back(1.0);
test.my_v.push_back(2.0);
// possible, handled by "operator=" of proxy
test[0] = 2.0;
// possible, handled by "operator T const& ()" of proxy
double x = test2[0];
// Possible, handled by "operator=" of proxy
test[0] = test2[1];
}
Compile Command
g++ -std=c++17 proxy.cpp -o proxy
Execution Command
./proxy
Output A
Writing.
Reading.
Reading.
Writing.
Comment A
Now add this class:
class myclass
{
public:
void xyzzy()
{
std::cout << "Xyzzy." << std::endl;
}
};
and change the main function accordingly while calling xyzzy
to test member access:
int main()
{
mycontainer<myclass> test;
mycontainer<myclass> const & test2(test);
test.my_v.push_back(myclass());
test.my_v.push_back(myclass());
// possible, handled by "operator=" of proxy
test[0] = myclass();
// possible, handled by "operator T const& ()" of proxy
myclass x = test2[0];
// Possible, handled by "operator=" of proxy
test[0] = test2[1];
// Test member access
test[0].xyzzy();
}
Output B
proxy.cpp: In function ‘int main()’:
proxy.cpp:70:11: error: ‘class myproxy<myclass, myclass, false>’ has no member named ‘xyzzy’
70 | test[0].xyzzy();
| ^~~~~
Comment B
One way to resolve this is to unconditionally inherit T
.
struct myproxy < T, T, false > : public myproxy<T const, T>, T
^^^
Output C
Writing.
Reading.
Reading.
Writing.
Xyzzy.
Comment C
However, unconditionally inheriting T
causes a different compile failure when we switch back to primitive types.
Output D
proxy.cpp: In instantiation of ‘class myproxy<double, double, false>’:
proxy.cpp:64:9: required from here
proxy.cpp:21:8: error: base type ‘double’ fails to be a struct or class type
21 | struct myproxy < T, T, false > : public myproxy<T const, T>, T
| ^~~~~~~~~~~~~~~~~~~~~~~
Comment D
We can probably conditionally inherit T
for structs and class types using std::enable_if
but I'm not proficient enough with C++ to know if this causes different underlying issues.
After a week of investigating and reviewing dozens and dozens of proxy pattern implementations I have discovered that almost every proxy pattern implementation is broken because of how the primary operator method(s) are written.
Case in point:
myproxy<T> operator[] (typename std::vector<T>::size_type const i)
^^^^^^^
This should be
T
. Obviously,T<T>
doesn't work here butT
does.In fact this should specifically be
T&
(to avoid subtle breakage, especially if we are using a map or map-like container as the underlying) but that doesn't work here either without rewriting the implementation.
But regardless of whether we use T
or T&
we'll get:
Output E
Reading.
Reading.
Reading.
Reading.
Reading.
Xyzzy.
Comment E
As you can see, we lost the ability to distinguish reads from writes.
Additionally, this method causes a different compile failure when we switch back to primitive types:
Output F
proxy.cpp: In function ‘int main()’:
proxy.cpp:64:13: error: lvalue required as left operand of assignment
64 | test[0] = 2.0;
| ^~~
proxy.cpp:68:20: error: lvalue required as left operand of assignment
68 | test[0] = test2[1];
|
Comment F
We can probably resolve this by adding another class to access the components as lvalues but I'm also not proficient enough with C++ to know if this causes different underlying issues.
Question
How do we distinguish reads from writes when using the proxy pattern without breaking (1) interaction with primitive types, and (2) member access for structs & class types?
There's no short answer to this one so if you don't understand the problem then start from the beginning otherwise start with Answer for Trivial Use Cases which addresses the original question.
Premise
You create a wrapper around two or more containers and want to support the
std::map
or map-like subscript operator[]
.Problem
You realize that when you insert values using the subscript operator
[]
that every underlying container must also receive this value. However, you discover that the subscript operator[]
doesn't know if it's reading or writing a value until after the function has returned.Without knowing the value you can't populate every underlying container so you search for ways to obtain the value.
"Solution"
You discover the proxy pattern and realize it's necessary since there is no other way to directly obtain the value.
You may even encounter some words by @KenBloom which emphasize the need for the proxy pattern, "C++ doesn't define a
[]=
operator like Ruby, a magicupdate
function like Scala, or parameterized properties like Visual Basic."However, you realize that use of the proxy pattern will break either (1) interaction with primitive types, or (2) member access for structs & class types.
Answer for Trivial Use Cases
Which brings us here.
@NicolBolas said, "C++ doesn't allow you to do the kind of thing you want to do. Any kind of proxy type is going to, at some point, not behave like the thing it is proxying. A C++ proxy can only ever be an approximation, not a replacement."
Only the first sentence is not true since all you have to do is conditionally inherit
T
.This makes
mycontainer
output correctly for trivial use cases (and addresses the original question).Trival meaning (1) as long as
mycontainer
is not used recursively, or (2) ifmycontainer
is used recursively then as long asmycontainer
is only used for the innermost node.If you use the proxy pattern and your output is mostly empty then your use case is non-trivial and the only way to get the expected output is to return
T&
but if you returnT&
you cannot use the proxy pattern.Answer for non-Trivial Use Cases
As previously mentioned, the subscript operator
[]
doesn't know if it's reading or writing a value until after the function has returned.You can try to figure out a way to execute code after the function has returned (which would probably be undefined behavior if you actually succeeded) or you can try to understand what returning
T&
means.T&
returns a reference backed by a memory address.A memory address is assigned when a variable is created.
This means we don't need the value. We only need a reference that will remain valid after the function returns.
Once the function returns the value will be assigned to the reference and therefore every underlying container that received the reference will have the value.
All you have to do is use a container that does not invalidate iterators or references as the base container.
For instance,
std::list
.