Объект Composite property, который блокирует множественные выбросы сигнала при изменении всех подобъектов

У меня есть простой класс property<T> с value_changed<T>, который вы можете connect/disconnect, и принимать или запрещать события при вызове value_changed<T>::emit(T). Подумайте о сигнале/слотах Qt на стероидах С++ 11.

Моя следующая задача - предоставить объект, подобный объекту, который состоит из под-свойств. Подумайте, например, о позиции или размере, которые состоят из нескольких значений. Я хотел бы иметь возможность обрабатывать подобъекты как property и дополнительно получать составной сигнал, излучаемый при одновременном изменении нескольких значений. Например. делать

struct 
{
  property<int> x;
  property<int> y;
}
position2d pos{0,0};
// ...
pos = {1,1}; // this should fire x.value_changed, y.value_changed, and pos.value_changed (once!)

Это последнее маленькое слово является ядром проблемы. Я изо всех сил пытаюсь закодировать повторно используемый composite_property, который можно настроить с помощью имен субобъектов (позиция будет получать x, y, но размер получит width/height).

Примечание a property<struct { int x; int y; }> не хватает: изменение x не будет выдавать композитный сигнал value_changed.

Лучшее, что я могу придумать, - это что-то с кучей кода шаблона для подключения/отключения подобъектов при назначении суперобъекту, который является утомительным и идет против принципа DRY.

Я открыт для волшебной магии шаблона, хотя я понимаю, что свободное присвоение переменных (x/y и width/height) сделает хотя бы некоторый код шаблона.

EDIT Для полноты, это шаблон property, который у меня есть сейчас:

template<typename T>
class property
{
public:
  using value_type = T;
  using reference = std::add_lvalue_reference_t<T>;
  using const_reference = std::add_lvalue_reference_t<std::add_const_t<T>>;
  using rvalue_reference = std::add_rvalue_reference_t<T>;

  property(const_reference value_ = {}) : value(value_) {}

  operator const_reference() const { return value; }

  property& operator=(const_reference& other)
  {
    const bool changed = value != other;
    value = other;
    if(changed)
      value_changed.emit(value);

    return *this;
  }

  bool operator==(const_reference other) const { return value == other; }
  bool operator!=(const_reference other) const { return value != other; }
  bool operator< (const_reference other) const { return value <  other; }
  bool operator<=(const_reference other) const { return value <= other; }
  bool operator> (const_reference other) const { return value >  other; }
  bool operator>=(const_reference other) const { return value >= other; }

  signal<value_type> value_changed;

  private:
    value_type value;
};

signal немного больше задействован и доступен здесь. В принципе, connect как Qt, за исключением того, что он возвращает объект connection_type, такой как Boost.Signal, который можно использовать для disconnect этого соединения.

Примечание. Я открыт для функции "изменять свойство без изменений", которая обходит сигнал, но только реализует половину того, что мне нужно.

Ответ 1

Поскольку вопрос отмечен , здесь простое решение, использующее некоторые блестящие новые возможности С++ 17 (по линиям, обсуждаемым в комментариях выше):

template<class T, auto... PMs> struct composite_property : property<T>
{
   using const_reference = typename property<T>::const_reference;

   composite_property(const_reference v = {}) : property<T>(v)
   {
      (... , (this->value.*PMs).value_changed.connect([this](auto&&)
         {
            if(listen_to_members) this->value_changed.emit(this->value);
         }));
   }

   composite_property& operator=(const_reference& other)
   {
      listen_to_members = false;
      property<T>::operator=(other);
      listen_to_members = true; // not exception-safe, should use RAII to reset
      return *this;
   }

private:
   bool listen_to_members = true;
};

Из чистой лени я внес изменения в ваш property<T>: я сделал публичный value. Конечно, есть несколько способов избежать этого, но они не связаны с проблемой, поэтому я решил сохранить все просто.

Мы можем протестировать решение, используя этот игрушечный пример:

struct position2d
{
   property<int> x;
   property<int> y;

   position2d& operator=(const position2d& other)
   {
      x = other.x.value;
      y = other.y.value;
      return *this;
   }
};

bool operator!=(const position2d& lhs, const position2d& rhs) { return lhs.x.value != rhs.x.value || lhs.y.value != rhs.y.value; }

int main() 
{
   composite_property<position2d, &position2d::x, &position2d::y> pos = position2d{0, 0};

   pos.value.x.value_changed.connect([](int x) { std::cout << " x value changed to " << x << '\n'; });
   pos.value.y.value_changed.connect([](int y) { std::cout << " y value changed to " << y << '\n'; });
   pos.value_changed.connect([](auto&& p) { std::cout << " pos value changed to {" << p.x << ", " << p.y << "}\n"; });

   std::cout << "changing x\n";
   pos.value.x = 7;
   std::cout << "changing y\n";
   pos.value.y = 3;
   std::cout << "changing pos\n";
   pos = {3, 7};
}

Здесь живой пример со всеми необходимыми определениями (мой код внизу).

При необходимости явно указывать элементы в качестве аргументов шаблона composite_property может раздражать, он также обеспечивает довольно гибкость. Мы можем иметь, например, класс с тремя свойствами элемента и определять различные составные свойства по различным парам свойств членов. Содержащий класс не затрагивается ничем из этого и может также работать независимо от каких-либо составных свойств, причем элементы используются как автономные свойства.

Обратите внимание, что существует причина для предоставленного пользователем оператора назначения копирования position2d: если мы оставим его по умолчанию, он скопирует сами свойства элемента, который не испускает сигналы, а скорее дублирует исходные свойства.

Код работает на соединительной линии Clang в режиме С++ 1z. Это также вызывает ICE на стволе GCC; мы должны попытаться уменьшить пример, чтобы получить то, что может быть представлено в отчете об ошибке.

Важнейшей функцией С++ 17 здесь является auto параметры шаблона непигового типа. В предыдущих версиях языка существуют несколько более уродливые альтернативы, например, перенос указателей на члены в виде ptr_to_mem<decltype(&position2d::x), &position2d::x>, возможно, с использованием макроса, чтобы избежать повторения.

В реализации конструктора composite_property существует также выражение fold ,, но это также можно сделать (несколько более подробным образом), инициализируя фиктивный массив.