复合属性对象,在所有子对象发生更改时禁止多个信号发射

rub*_*nvb 10 c++ signals composite c++17

我有一个简单的property<T>一类value_changed<T>,你可以connect/ disconnect和接收或抑制的事件时value_changed<T>::emit(T)被调用.想想C++ 11类固醇上的Qt信号/插槽.

我的下一个挑战是提供一个由子属性组成的类属性对象.例如,考虑一个位置或大小,它们都包含多个值.我希望能够将子对象视为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!)
Run Code Online (Sandbox Code Playgroud)

最后一个小字是问题的核心.我竭力要编写一个可重复使用 composite_property,可与子对象的名称进行定制(位置会得到x,y但尺寸会得到width/ height).

注意a property<struct { int x; int y; }>不够:更改x不会发出复合value_changed信号.

我能想出的最好的东西是带有一堆样板代码,用于在分配给超级对象时连接/断开子对象,这很乏味并且违反DRY原则.

我对野生模板魔法持开放态度,虽然我理解变量(x/ ywidth/ height)的自由命名将至少需要一些样板代码.

编辑为了完整性,这是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;
};
Run Code Online (Sandbox Code Playgroud)

signal有点参与,可以在这里找到.基本上,connect像Qt一样,除了它返回一个connection_type像Boost.Signal 这样的对象,它可以用于disconnect那个连接.

注意我打开后门"默认修改属性"功能绕过信号,但这只能实现我需要的一半.

bog*_*dan 4

由于问题被标记为,因此这里有一个简单的解决方案,它使用一些闪亮的新 C++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;
};
Run Code Online (Sandbox Code Playgroud)

出于纯粹的懒惰,我对你的进行了更改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};
}
Run Code Online (Sandbox Code Playgroud)

这是一个包含所有必要定义的实例(我的代码位于底部)。

虽然必须将成员显式列出为composite_property模板的参数可能很烦人,但它也提供了相当多的灵活性。例如,我们可以拥有一个具有三个成员属性的类,并在不同的成员属性对上定义不同的复合属性。包含类不受任何此类影响,并且还可以独立于任何复合属性工作,并将成员用作独立属性。

请注意,用户提供的复制赋值运算符 of 是有原因的position2d:如果我们将其保留为默认值,它将复制成员属性本身,这不会发出信号,而是会复制源属性。

该代码在 C++1z 模式下在 Clang trunk 上运行。它还会导致 GCC 主干上出现 ICE;我们应该尝试减少示例以获得可以在错误报告中提交的内容。

这里起作用的关键 C++17 功能auto是非类型模板参数。在以前的语言版本中,有一些更丑陋的替代方案,例如,将指向成员的指针包装在类似 的内容中ptr_to_mem<decltype(&position2d::x), &position2d::x>,可能使用宏来避免重复。

,在 的构造函数的实现中还有一个折叠表达式composite_property,但这也可以通过初始化虚拟数组来完成(以稍微更详细的方式)。