6
\$\begingroup\$

(There is now a 2nd version of this code)

Have you ever written proxy objects, instead of using a setter and a getter method? In that case, I'm interested in your opinion on the following design for a templated proxy:

#include <type_traits>
#include <utility>

template <typename Handle, typename Getter, typename Setter>
class proxy {
public:
    using value_type = decltype(std::declval<Getter>()(std::declval<Handle>()) );
    operator value_type() const { return getter_(handle_); }
    proxy& operator=(const value_type& x) { setter_(handle_, x); return *this; }
    proxy& operator=(const value_type&& x) { setter_(handle_, x); return *this; }
    proxy(Handle handle, const Getter& getter, const Setter& setter) : 
    handle_(handle), getter_(getter), setter_(setter) { }
protected:
    const Handle handle_;
    const Getter& getter_;
    const Setter& setter_;
};

// Allows for template argument deduction during "construction" - before C++17
template <typename Handle, typename Getter, typename Setter>
proxy<Handle, Getter, Setter>
make_proxy(const Handle& handle, const Getter& getter, const Setter& setter)
{
    return proxy<Handle, Getter, Setter>(handle, getter, setter);
} 

Simple example of use:

int my_getter(int *x) { return *x; }
void my_setter(int *x, int val ) { *x = val; }

class foo {
public:
    auto datum() { 
        return make_proxy(&x, my_getter, my_setter);
    }
protected:
    int x { 123 };
};

int main() {
    foo my_foo;
    my_foo.datum() = 456;
    return my_foo.datum();
}

GodBolt

(in this example, the getter and setter aren't really necessary because the "raw" field exists. But think about opqaue operating-system resources, or individual bits in a bit-container etc.)


Other than general comments on the design - I was also thinking about the choice of template parameters. I might be able to drop the Handle type - if I could manager to extract that information from Getter; or alternatively, I could add the value_type as a template parameter - as otherwise it could be confusing to the person seeing an instantiation to understand what type they should actually use with the proxy.

Also, I was wondering whether I should provide comparators (seeing how this class is convertible to value_type's, which we should be able to compare).

Finally, I was also thinking of not keeping the getter_ and setter_ at all, and only instantiating them on use. But I'm worried this will make the class too convoluted to write and/or use.

Note: This needs to be C++11-compatible.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Note to people in the Close Vote Queue, there is nothing wrong with this code. \$\endgroup\$
    – pacmaninbw
    Commented Jul 20, 2020 at 14:48

2 Answers 2

4
\$\begingroup\$

It's always going to be ugly

Getters and setters are never going to be as pretty in C++ as in some other languages that natively support them. Maybe something nice will be possible in a future C++ standard, but certainly not if you are stuck with C++11.

If you are going to use this to proxy member variables of a class, then the best syntax I can come up with is:

class foo {
    int x{123};
    static int getter(foo *self) { return self->x; }
    static void setter(foo *self, int val) { self->x = val; }
public:
    auto datum() { return make_proxy(this, getter, setter); }
};

The above allows the getter and setter access to the whole class, in case it wants to update multiple variables, or if the getter returns some function of multiple member variables. But unless you can reuse getters and setters, there is really not much point to it in my opinion.

The main reason is that either you have to write my_foo.datum() = 456, which has the added parentheses that make it not look like you are setting a regular member variable, or you have to declare datum like so:

class foo {
    ...
public:
    proxy<foo *, decltype(getter), decltype(setter)> datum{this, getter, setter};
};

That will allow you to write my_foo.datum = 456.

The former doesn't even work with C++11, since auto return type deduction doesn't work in that situation. You could add a trailing return type, but it will be ugly and repetetive. The latter has the overhead of storing three pointers for each proxy variable in your class.

It doesn't handle const instances

Your proxy class doesn't work if you create a const instance of a class that uses proxy member variables, for example:

const foo my_foo;
return my_foo.datum();

You could probably create a class const_proxy that only has a getter, and which ensures const is used in the right places, and then overload the proxy member like so:

class foo {
    ...
public:
     auto datum() { return make_proxy(this, getter, setter); }
     auto datum() const { return make_const_proxy(this, getter); }
};

But that adds even more noise.

\$\endgroup\$
4
  • \$\begingroup\$ You're reviewing my example of use rather than my general proxy template. The example is just an example... indeed, it doesn't have the const variant of the proxy-returning method - but you'll note I have planned for const-correctness in proxy itself. Now, about make_const_proxy - that might indeed by necessary, especially since we don't have std::add_const() like in C++17; but a separate const_proxy class would not be necessary - const proxy should do well enough. Now, as for keeping the proxy as a member variable - that costs a bunch of storage. What if I have a bliion foo's? \$\endgroup\$
    – einpoklum
    Commented Jul 20, 2020 at 15:24
  • 2
    \$\begingroup\$ But how you use it is the most important thing! I can write code which looks and works perfectly, but if it doesn't do anything useful, what is the point? The point of a proxy template is that it makes getters and setters easier, but if you have to add several lines of code for each proxied variable just to make that happen, its usefulness is severely diminished. I tried creating a proxy class myself a while ago, but I failed to make it practical, see: stackoverflow.com/questions/57531691/… \$\endgroup\$
    – G. Sliepen
    Commented Jul 20, 2020 at 15:32
  • 1
    \$\begingroup\$ If you come up with a version of your proxy class that also works when creating const instances of a class that uses proxies, then please create a new review for it. I did mention that keeping the proxy as a member variable costs you three pointers worth of storage, so indeed it is not very practical. \$\endgroup\$
    – G. Sliepen
    Commented Jul 20, 2020 at 15:38
  • 1
    \$\begingroup\$ The "version" is the same proxy itself. See here. But it's true that this makes the use a little uglier. I haven't quite managed to write a make_const_proxy() that doesn't drop constness somehow. \$\endgroup\$
    – einpoklum
    Commented Jul 20, 2020 at 16:45
3
\$\begingroup\$

It lacks a very important functionality - the proxy should be initializable via lambdas or closures. With current API it is impossible to do properly.

For instance, consider following code:

double x=1;
double coef=2.;
auto pr = make_proxy(&x,
                [coef](double*x){return *x*coef;}, 
                [coef](double*x, double nx){*x = nx/coef;});
pr = 8.;

Now, this a nice and simple piece of code where proxy's setter/getter multiply/divide the value by coef. Only issue is that this code is UB. You see these lambdas instances get destroyed past make_proxy because proxy stores only const references which will become dangling and their use turns into UB.

Note: the Handle variable only incoveniences writing generation of the proxy. Without it, it would be easier to user to write lambdas. If you worry about functions/method then you can simply use std::bind for wrapping those up.

Technical issue:

    proxy& operator=(const value_type&& x) { setter_(handle_, x); return *this; }

It is not a proper move-assignment implementation. I just write a proper one:

    proxy& operator=(value_type&& x) { setter_(handle_, std::move(x)); return *this;}

(Also frequently these ought to be noexcept - but uncertain whether it suitable for the current case). Also there is little point in writing move+copy assignment operator unless you define them for the current class. Both of them can be implememted via a single definition as:

   proxy& operator = (value_type x) { setter_(handle_, std::move(x)); return *this;}

Some may argue that this is slower but compiler should be able to optimize out the inefficiencies.

Also you should consider the case where getter returns a const reference instead of the value.

\$\endgroup\$
8
  • \$\begingroup\$ Thanks for the suggestions. 1. "the proxy should be initializable via lambdas / closures." <- Can you elaborate on this point? 2. I wasn't thinking about noexcept because in my mind this always gets inlined, but I probably should. 3. I'm not sure about your move+copy assignment, since value_type will generally not be an rvalue reference. And the compiler probably won't be able to optimize this away if there are side-effects involved. \$\endgroup\$
    – einpoklum
    Commented Jul 21, 2020 at 8:04
  • \$\begingroup\$ @einpoklum 1. instead of getter/setter functions it is more reasonable to assume that user will make some simple lambdas. E.g., int x; auto getter =[&x](){return x;}; auto setter = [&x](int x2){x=x2;}; and no need for handle. Now how to make proxy from these? \$\endgroup\$
    – ALX23z
    Commented Jul 21, 2020 at 8:12
  • \$\begingroup\$ The make_proxy() function should handle that, I think... Also, it's not that likely lambdas will be used this way, since it's mostly class authors who will need this proxy, and they can just use plain vanilla functions/methods within the actual source file. \$\endgroup\$
    – einpoklum
    Commented Jul 21, 2020 at 8:15
  • \$\begingroup\$ @einpoklum 2. Hmm... normally you don't want to have side effects in copy-consructor. If a class has complex construction with side effects then presumably it is some function module and not data storage in which normally one simply stores them in some smart pointer and never moves/copies/assigns it. So I find it strange that somebody would want to have getter/setter proxy for it. \$\endgroup\$
    – ALX23z
    Commented Jul 21, 2020 at 8:19
  • \$\begingroup\$ R (2.) : Granted, but this is a generic proxy, so I want to minimize my assumptions. \$\endgroup\$
    – einpoklum
    Commented Jul 21, 2020 at 8:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.