Properties in C++20

Properties in C++20

C++ doesn't support getter/setter style properties. But you can get pretty darn close with templates or macros.

#include <iostream>
#include "properties.h"

using namespace std;

class User
{
private:
  int mId;
  int mWeight;
  int mPurse;
  int mBank;
public:
  // Non-auto property only supporting get (runtime check)
  Property<int> Id{{ .get = mId }};
  Property<int> Purse {{ .set = mPurse }};
  Property<int> Bank {{ .set = mBank }};

  Property<int> Wealth {{ .get = [this]() { return mPurse + mBank; }}};

  // Auto-properties
  Property<string> FirstName;
  Property<string> LastName;

  // Non-auto property
  Property<string> FullName {{
    .get = [this]() { return LastName + string(", ") + FirstName; }
  }};

  // Property backed by member variables
  Property<int> Weight { mWeight };

  // Non-auto property with value checks
  Property<int> Age {{
    .get = [](int value) { return value; },
    .set = [](int& value, int newValue) {
      if (newValue < 0) newValue = 0;
      if (newValue > 150) newValue = 150;
      return value = newValue;
    }
  }};

  User(int id) { mId = id; }
};

int main()
{
  auto user = User(0);
  cout << "First Name: ";
  cin >> user.FirstName;
  cout << "Last Name: ";
  cin >> user.LastName;
  user.Age = 46;
  cout << "Full Name: " << user.FullName << endl;
  user.Age++;
  cout << "Age: " << user.Age << endl;
  // user.Id = 10; // Throws an exception
  user.Weight = 205;
  user.Purse = 20;
  user.Bank = 100;
  cout << "Wealth: " << user.Wealth;
  // user.Wealth = 100; // Throws an exception
}

It's not perfect but it's close.

We can get the above by extending Peter Dimov's Named Parameters in C++20 post and the templating approach for C++ properties on Wikipedia:

#include <iostream>

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wreturn-stack-address"

template <typename T>
class Property
{
  T _value;
  bool _isAuto = true;
  /**
   * Used for auto-property behavior.
   */
  std::function<const T&(const T&)> _autoGetter = [](const T& value) -> const T& { return value; };
  std::function<const T&(T&, const T&)> _autoSetter = [](T& value, const T& newValue) -> const T& { return value = newValue; };

  /**
   * Used for non-auto property behavior.
   */
  std::function<const T&()> _getter;
  std::function<const T&(const T&)> _setter;

  /**
   * Convenience methods for calling the actual setter/getters regardless of auto-property
   * or not.
   */
  const T& set(const T& value) { return _isAuto ? _autoSetter(_value, value) : _setter(value); }
  const T& get() { return _isAuto ? _autoGetter(_value) : _getter(); }

public:

  struct AutoParams {
    std::function<const T&(const T&)> get = [](const T& value) { return value; };
    std::function<const T&(T&, const T&)> set = [](T& value, const T& newValue) { return value = newValue; };
  };

  struct Params {
    std::function<const T&()> get = []() -> const T& { throw new std::exception(); };
    std::function<const T&(const T&)> set = [](const T& value) -> const T& { throw new std::exception(); };
  };

  struct WrappedGetParams {
    T& get;
  };

  struct WrappedSetParams {
    T& set;
  };

  // Implicit conversion back to T.
  operator const T& () { return get(); }

  const T operator=(T other) { return set(other); }
  const T operator=(Property<T> other) { return set(other.get()); }

  Property<T>& operator++() { return set(get()++); }
  T operator++(int n) {
    return set(get() + (n != 0 ? n : 1));
  }
  Property<T>& operator--() { return set(get()--); }
  T operator--(int n) {
    return set(get() - (n != 0 ? n : 1));
  }
  
  const T& operator+=(const T& other) { return set(get() + other); }
  const T& operator-=(const T& other) { return set(get() - other); }

  const T& operator+(const T& other) { return get() + other; }
  friend const T& operator+(const T& first, Property<T>& other) { return first + other.get(); }

  const T& operator-(const T& other) { return get() - other; }
  friend const T& operator-(const T& first, Property<T>& other) { return first - other.get(); }

  const T& operator*(const T& other) { return get() * other; }
  friend const T& operator*(const T& first, Property<T>& other) { return first * other.get(); }

  const T& operator/(const T& other) { return get() / other; }
  friend const T& operator/(const T& first, Property<T>& other) { return first / other.get(); }

  friend std::ostream& operator<<(std::ostream& os, Property<T>& other) { return os << other.get(); }
  friend std::istream& operator>>(std::istream& os, Property<T>& other) {
    if (other._isAuto) {
      return os >> other._value;
    }
    else {
      T ref;
      os >> ref;
      other.set(ref);
      return os;
    }
  }

  // This template class member function template serves the purpose to make
  // typing more strict. Assignment to this is only possible with exact identical types.
  // The reason why it will cause an error is temporary variable created while implicit type conversion in reference initialization.
  template <typename T2> T2& operator=(const T2& other)
  {
    T2& guard = _value;
    throw guard; // Never reached.
  }

  Property() {}

  Property(T& value)
  {
    _isAuto = false;
    _getter = [&]() -> const T& { return value; };
    _setter = [&](const T& newValue) -> const T& { return value = newValue; };
  }

  Property(AutoParams params)
  {
    _isAuto = true;
    _autoGetter = params.get;
    _autoSetter = params.set;
  }

  Property(Params params)
  {
    _isAuto = false;
    _getter = params.get;
    _setter = params.set;
  }

  Property(WrappedGetParams params)
  {
    _isAuto = false;
    auto get = params.get;
    _getter = [get]() { return get; };
    _setter = [](const T& newValue) -> const T& { throw new std::exception(); };
  }

  Property(WrappedSetParams params)
  {
    _isAuto = false;
    T& set = params.set;
    _getter = []() -> const T& { throw new std::exception(); };
    _setter = [&set](const T& newValue) { return set = newValue; };
  }
};

#pragma GCC diagnostic pop
See this gist on GitHub
Show Comments