2
\$\begingroup\$

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.

This is a second version, after addressing comments from a previous review; it is intended to work with C++11 or newer. Issues which have been addressed + changes:

  • Support for const proxies.
  • Move constructor.
  • The getter possibly returning a reference, or some other funny type.
  • noexcept() decoration
  • constexpr decoration
#include <type_traits>
#include <utility>


#if __cplusplus >= 201402L
#define CONSTEXPR_SINCE_CPP14 constexpr
#else
#define CONSTEXPR_SINCE_CPP14
#endif


template <typename Handle, typename Getter, typename Setter>
class proxy {
public:
    using getter_return_type = decltype(std::declval<Getter>()(std::declval<Handle>()) );
        // Note: We assume the getter does not allow modifying the value. If it does - you don't
        // need the proxy in the first place. But _asserting_ this is difficult. For example, suppose
        // the getter returns an int*. Is the "actual" value an int, or an int*? We can't know that
        // without introducing yet another type parameter.

    using value_type = typename std::remove_reference<typename std::remove_cv<getter_return_type>::type>::type;

    CONSTEXPR_SINCE_CPP14 
    operator getter_return_type() const noexcept(noexcept(getter_)) 
    { return getter_(handle_); }

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

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

    CONSTEXPR_SINCE_CPP14 
    proxy(Handle handle, const Getter& getter, const Setter& setter) noexcept 
        : handle_(handle), getter_(getter), setter_(setter) { }

protected:
    const Handle handle_; 
        // Note: The handle is constant, not the referred-to element. So, _don't_ use a `T&` as your Handle, or
        // you'll be in trouble
    const Getter& getter_;
    const Setter& setter_;
        // Note: Attempts to use just plain Getter and Setter as the types here don't work when Getter/Setter
        // are function types
};

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

And here are a couple of examples:


template <class T>
constexpr typename std::remove_const<T>::type& as_non_const(T& t) noexcept
{
    return const_cast<typename std::remove_const<T>::type&>(t);
}

// This exists in C++14, but this example is compiled with C++11
template <class T>
constexpr typename std::add_const<T>::type& as_const(T& t) noexcept
{
    return t;
}

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

class foo {
public:
    using proxy_type = proxy<int*, decltype(my_getter), decltype(my_setter)>;
    proxy_type datum() {  return make_proxy(&x, my_getter, my_setter);  }
    const proxy_type datum() const { 
        return make_proxy(&as_non_const(x), my_getter, my_setter);
    }
protected:
    int x { 123 };
};

int example1()
{
    foo my_foo;

    my_foo.datum() = 456;
    return as_const(my_foo).datum();
}

double example2()
{
    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.;
    return as_const(pr);
}

See it all on GodBolt.

Other than general comments, I'd appreciate help with:

  • What do you think of my constexpr'ization? Is it reasonably executed? Should I even bother with it?
  • Ditto regarding the use of noexcept().
  • Can I, and should I, make the getter_ and setter_ members into possibly-non-reference types?
  • Is there any use to accepting the value type as an explicit template parameter?
  • What if someone wants to use a T& as the handle? i.e. use x as the handle in the examples rather than &x, with appropriate getter and setter?
  • What are your thoughts on comparison operators?
\$\endgroup\$
0
\$\begingroup\$

std::ref to the rescue

I believe that most of the places where references are used could be replaced with value type in the class and the user could just std::ref() their objects/callables (in this specific case, not in general). std::reference_wrapper provides implicit conversion to reference type (one of the main use cases include passing references to callables for std::thread and std::bind, as it decay copies by default). The other useful part of the wrapper is call operator, e.g. operator()(...args). It takes whatever arguments are passed to it and simply forwards to the wrapped object.

At first glance it seems like there is undefined behavior in the example, as make_proxy is called with rvalues whose lifetime expires at the end of the expression, but they are still used afterwards.

\$\endgroup\$
7
  • \$\begingroup\$ 1. The user should not need to know about std::ref() or std::reference_wrapper() - I don't want to complicate their life. If relevant, I would need to take care of it within the class ctor and make_proxy. \$\endgroup\$
    – einpoklum
    Nov 9 '21 at 12:52
  • \$\begingroup\$ @einpoklum it a decision users have to make conscious choice about. Trying to handle it in the class will probably lead to a tag type being required to differentiate between them or providing a different overload. The class just cannot make the decision without users help. If there is anything I am missing, perhaps you could provide slightly more specific example? \$\endgroup\$ Nov 9 '21 at 17:52
  • \$\begingroup\$ " undefined behavior in the example, as make_proxy is called with rvalues..." <- Won't the lifetime be extended by the const-references? \$\endgroup\$
    – einpoklum
    Nov 9 '21 at 18:33
  • \$\begingroup\$ "it a decision users have to make conscious choice about. " <- disagree. Library users don't make conscious decisions about most of what you offer them... think of std::variant for example. There are lots of architectural choices in there which users are kept in the dark about. And that's fine. For this kind of a proxy class to have general appeal, it has to be simple and straightforward to use; and I won't shy away from a tag type for differentiating different setter and getter types, if it helps make the proxy easier to use from the outside. \$\endgroup\$
    – einpoklum
    Nov 9 '21 at 18:36
  • \$\begingroup\$ @einpoklum about lifetime and const reference: think of it as NRVO/RVO. If you can apply RVO, then it will be extended. When the direction is inside the function the lifetime will be equal to expression containing the function call. \$\endgroup\$ Nov 9 '21 at 18:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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