TransWikia.com

A proxy replacement for getters and setters

Code Review Asked on October 27, 2021

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.

2 Answers

It lacks a very important functionality - the proxy should be initializable via lambdas / 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, fouble 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 lamdas 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 incoviniences 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.

Answered by ALX23z on October 27, 2021

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.

Answered by G. Sliepen on October 27, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP