6
\$\begingroup\$

I'm studying metaprogramming I figured out a way to implement C#-like properties in C++. This is proxy object that performs a specified function when you try to receiving or assigning values.

#ifndef PROPERTY_H
#define PROPERTY_H

#include <functional>

template <typename T> 
using get_function = std::function<const T& ()>;

template <typename T> 
using set_function = std::function<void(const T&)>;

template <typename T>
class get_property {
public:
    constexpr get_property(get_function<T> get) : get_f(get){}
    operator const T& () const { return get_f(); }
private:
    get_function<T> get_f;
};

template <typename T>
class set_property {
public:
    constexpr set_property(set_function<T> set) : set_f(set) {}
    void operator= (const T& value) { set_f(value); }
private:
    set_function<T> set_f;
};


template <typename T>
class property : private get_property<T>, private set_property<T> {
public:
    constexpr property(get_function<T> get, set_function<T> set) : get_property<T>(get), set_property<T>(set) {}
    using get_property<T>::operator const T&;
    using set_property<T>::operator=;
};


#endif // PROPERTY_H

It can be used like this:

class User {
public:
    // You can get or set value
    property<int> value = {[this]() { return data; }, [this](int v) { data = v; }}; 

    // You can only get value
    get_property<int> get_value = {[this]() { return data; }};

    // You can only set value
    set_property<int> set_value = {[this](int v) { data = v; }};
private:
    int data = 42;
};

I would like to find a way to integrate declarations property, get_property and set_property into one. How can I determine object type from its constructor?

\$\endgroup\$
  • 2
    \$\begingroup\$ If you have a public set/get combo for a variable, you are essentially making that variable public and breaking encapsulation. Sure having accessors allows you to hook the change for example but this doesn't diminish the fact that internal state of the class is publicly available. To me this is a code smell in most cases (there are always exceptions to all rules of course). If you have some piece of code that needs to set/get something on a class, ask yourself this question: "Why is this class accessing my private state? Can't that functionality be moved to this class instead?" \$\endgroup\$ – Emily L. Jul 13 '15 at 18:50
  • \$\begingroup\$ Getters and Setters break encapsulation. And even worse result in tight coupling of your class to other classes resulting in less code flexibility and higher maintenance cost. There is a reason they are not an automatic part of the language (they are a bad idea). \$\endgroup\$ – Martin York Jul 13 '15 at 23:24
6
\$\begingroup\$

I would like to find a way to integrate declarations "property, get_property and set_property" into one. How can I determine object type from its constructor?

Don't. Seriously. No no no. Everything about this is bad code design from the bottom up and you can object to it for every reason. Even allowing for the idea that you should use getters and setters (which you shouldn't), this is an especially poor of way of implementing them.

It's slow. You're adding an unnecessary indirection on your getters and setters through std::function. That's going to take longer than a simple const T& getVal() const { return t; }

It's unreadable. Anybody can look at the above getter and understand what it does. People will look at your properties and be confused for a long time.

It adds a lot of bloat to the class. sizeof(User) should be 4 if it just has an int. Adding your properties makes it 72!! (Looking at just your property<int>).

It's unreadable. I'm repeating this. It doesn't even save you keystrokes!

property<int> value = {[this]() { return data; }, [this](int v) { data = v; }}; 
const int& getData() const { return data; } void setData(int v) { data = v; }

There is no reason to do it. Furthermore, how would you handle multiple properties of the same type?

In the interests of actually providing code critique, I want to address your get_property (your set_property has similar issues):

template <typename T>
class get_property {
public:
    constexpr get_property(get_function<T> get) : get_f(get){}
    operator const T& () const { return get_f(); }
private:
    get_function<T> get_f;
};

You have a constexpr constructor but you'll never be able to use this as a core constant expression anyway, so I don't see the point. You're also incurring an unnecessary copy to construct your get_f, and the alias isn't particularly useful. Prefer to let the function be constructed from anything:

template <typename T>
class get_property {
public:
    template <typename F,
              typename = std::enable_if_t<!std::is_base_of<get_property, std::decay_t<F>>::value>
              >
    get_property(F&& f) : get_f(std::forward<F>(f)) { }
    operator const T& () const { return get_f(); }
private:
    std::function<const T&()> get_f;
};
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Technically illegal, but in practice you can use zero-sized arrays to make properties not take up any space. \$\endgroup\$ – o11c Jul 13 '15 at 16:08
  • \$\begingroup\$ Template parameters don't take up any space at runtime. \$\endgroup\$ – o11c Jul 13 '15 at 16:11
  • \$\begingroup\$ @o11c: Legal in C not in C++ and the compiler tells you so (and fails to compile if you have -Werror set like you should). \$\endgroup\$ – Martin York Jul 13 '15 at 23:23
  • \$\begingroup\$ @LokiAstari No, it's illegal in both, albeit for different reasons. And that's what #pragma GCC diagnostic push/ignored/pop is for - the times when you know better for one specific site. \$\endgroup\$ – o11c Jul 13 '15 at 23:25
  • \$\begingroup\$ The only good use of zero zied arrays is SFINAE but not sure how it relates to this question. \$\endgroup\$ – Martin York Jul 14 '15 at 1:12
4
\$\begingroup\$

So your alternatives are:

// For Get
get_property<T> get_value = {[this]() { return data; }};
// or
T const& getData() const& {return data;}

// For Set
set_property<T> set_value = {[this](int v) { data = v; }};
// or
void setData(T&& value)   {data = value;}

// For Get/Set
property<T> value = {[this]() { return data; }, [this](int v) { data = v; }}; 
// or
T const& getData() const&   {return data;}
void     setData(T&& value) {data = value;}
  1. Not sure that it enhances readability any.
  2. Not sure that it enhances maintainability.
  3. Unless the code is part of a framework where get/set are used by the framework this breaks encapsulation because you are basically allowing direct access to the internal state of the object.
  4. This break OO principles.
    a) You are making your users retrieve the state.
    b) modify the state.
    c) then update the state.
    You should prefer to write methods that manipulate the state of the object rather than letting external entities modify the state of the object. This makes it easier to make sure the state of the object is kept consistent because it is only the class that maintains the state of the object (no external entities are used).
  5. It tightly couples any classes that uses your class to use type T.
| improve this answer | |
\$\endgroup\$
-1
\$\begingroup\$

This answer shares absolutely no code with yours. It does, however, do what you are trying to do without any of the major problems in your implementation.

My implementation is rather straightforward, it simply places a zero-sized object and casts that whenever you assign or read from it. All the usual problems with operator T() apply of course.

There is one major danger: the properties must be declared at offset 0 within the class. This precludes use with. Technically are ways to avoid it, but that majorly complicates the per-property definition ...

// library header
template<class Self, class T, T (Self::*getter)() const, void (Self::*setter)(T)>
class property
{
    char zero_sized[0];
public:
    operator T() const
    {
        const Self *self = reinterpret_cast<const Self *>(this);
        return (self->*getter)();
    }
    void operator = (T v)
    {
        Self *self = reinterpret_cast<Self *>(this);
        (self->*setter)(v);
    }
};


// user header
#include <algorithm>

using std::min;
using std::max;

template<class T>
struct Point
{
    T x, y;
};

template<class T>
struct Rectangle
{
    T get_width() const { return right - left; }
    void set_width(T w) { right = left + w; }
    property<Rectangle, T, &Rectangle::get_width, &Rectangle::set_width> width;
    T get_height() const { return bottom - top; }
    void set_height(T h) { bottom = top + h; }
    property<Rectangle, T, &Rectangle::get_height, &Rectangle::set_height> height;

    T top, bottom, left, right;

    Rectangle(Point<T> a, Point<T> b)
    : top(min(a.y, b.y)), bottom(max(a.y, b.y))
    , left(min(a.x, b.x)), right(max(a.x, b.x))
    {}
};


// main file
#include <cassert>

int main()
{
    Rectangle<int> r({1, 2}, {3, 4});
    int w = r.width;
    r.height = 3;
    assert (w == 2);
    assert (r.bottom == 5);
    assert (sizeof(r) == 4 * sizeof(int));
}
| improve this answer | |
\$\endgroup\$

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.