繁体   English   中英

使用代理模式时如何区分读取和写入而不破坏与原始类型的交互和其他类型的成员访问?

[英]How do I distinguish reads from writes when using the Proxy Pattern without breaking interaction with primitive types & member access for other types?

前言

经过一周的调查和审查数十个代理模式实现后,我提出了这个问题。

请不要错误地将这个问题标记为重复问题,除非答案不会破坏 (1) 结构和 class 类型的成员访问以及 (2) 与原始类型的交互。

代码

对于我最小的、可重现的示例,我使用来自 @Pixelchemist 的代码作为基础。

#include <vector>
#include <type_traits>
#include <iostream>

template <class T, class U = T, bool Constant = std::is_const<T>::value>
class myproxy
{
protected:
  U& m_val;
  myproxy& operator=(myproxy const&) = delete;
public:
  myproxy(U & value) : m_val(value) { }
  operator T & ()
  {
    std::cout << "Reading." << std::endl;
    return m_val;
  }
};

template <class T>
struct myproxy < T, T, false > : public myproxy<T const, T>
{
  typedef  myproxy<T const, T> base_t;
public:
  myproxy(T & value) : base_t(value) { }
  myproxy& operator= (T const &rhs)
  {
    std::cout << "Writing." << std::endl;
    this->m_val = rhs;
    return *this;
  }
};

template<class T>
struct mycontainer
{
  std::vector<T> my_v;
  myproxy<T> operator[] (typename std::vector<T>::size_type const i)
  {
    return myproxy<T>(my_v[i]);
  }
  myproxy<T const> operator[] (typename std::vector<T>::size_type const i) const
  {
    return myproxy<T const>(my_v[i]);
  }
};

int main()
{
  mycontainer<double> test;
  mycontainer<double> const & test2(test);
  test.my_v.push_back(1.0);
  test.my_v.push_back(2.0);
  // possible, handled by "operator=" of proxy
  test[0] = 2.0;
  // possible, handled by "operator T const& ()" of proxy
  double x = test2[0];
  // Possible, handled by "operator=" of proxy
  test[0] = test2[1];
}

编译命令

g++ -std=c++17 proxy.cpp -o proxy

执行命令

./proxy

Output A

Writing.
Reading.
Reading.
Writing.

评论 A

现在添加这个 class:

class myclass
{
public:
  void xyzzy()
  {
    std::cout << "Xyzzy." << std::endl;
  }
};

并在调用xyzzy测试成员访问时相应地更改主 function :

int main()
{
  mycontainer<myclass> test;
  mycontainer<myclass> const & test2(test);
  test.my_v.push_back(myclass());
  test.my_v.push_back(myclass());
  // possible, handled by "operator=" of proxy
  test[0] = myclass();
  // possible, handled by "operator T const& ()" of proxy
  myclass x = test2[0];
  // Possible, handled by "operator=" of proxy
  test[0] = test2[1];
  // Test member access
  test[0].xyzzy();
}

Output B

proxy.cpp: In function ‘int main()’:
proxy.cpp:70:11: error: ‘class myproxy<myclass, myclass, false>’ has no member named ‘xyzzy’
   70 |   test[0].xyzzy();
      |           ^~~~~

评论 B

解决此问题的一种方法是无条件继承T

struct myproxy < T, T, false > : public myproxy<T const, T>, T
                                                           ^^^

Output C

Writing.
Reading.
Reading.
Writing.
Xyzzy.

评论 C

但是,当我们切换回原始类型时,无条件继承T会导致不同的编译失败。

Output D

proxy.cpp: In instantiation of ‘class myproxy<double, double, false>’:
proxy.cpp:64:9:   required from here
proxy.cpp:21:8: error: base type ‘double’ fails to be a struct or class type
   21 | struct myproxy < T, T, false > : public myproxy<T const, T>, T
      |        ^~~~~~~~~~~~~~~~~~~~~~~

评论 D

我们可能可以使用std::enable_if有条件地为结构和 class 类型继承T但我对 C++ 不够熟练,无法知道这是否会导致不同的潜在问题。

经过一周的调查和审查几十个代理模式实现后,我发现几乎每个代理模式实现都被破坏了,因为主要操作符方法的编写方式。

一个例子:

myproxy<T> operator[] (typename std::vector<T>::size_type const i)
^^^^^^^
  1. 这应该是T 显然, T<T>在这里不起作用,但T起作用。

  2. 事实上,这应该是T& (为了避免细微的损坏,特别是如果我们使用 map 或类似地图的容器作为底层),但如果不重写实现,这在这里也不起作用。

但无论我们使用T还是T&我们都会得到:

Output E

Reading.
Reading.
Reading.
Reading.
Reading.
Xyzzy.

评论 E

如您所见,我们失去了区分读取和写入的能力。

此外,当我们切换回原始类型时,此方法会导致不同的编译失败:

Output F

proxy.cpp: In function ‘int main()’:
proxy.cpp:64:13: error: lvalue required as left operand of assignment
   64 |   test[0] = 2.0;
      |             ^~~
proxy.cpp:68:20: error: lvalue required as left operand of assignment
   68 |   test[0] = test2[1];
      |

评论 F

我们可以通过添加另一个 class 来以左值访问组件来解决此问题,但我对 C++ 也不够熟练,无法知道这是否会导致不同的潜在问题。

问题

在使用代理模式时,我们如何区分读取和写入而不破坏(1)与原始类型的交互,以及(2)结构和 class 类型的成员访问?

这个问题没有简短的答案,所以如果您不理解问题,请从头开始,否则从解决原始问题的简单用例答案开始。

前提

您围绕两个或更多容器创建包装器并希望支持std::map或类似地图的下标运算符[]

问题

您意识到,当您使用下标运算符[]插入值时,每个底层容器也必须接收该值。 但是,您发现下标运算符[]直到 function 返回之后才知道它是在读取还是写入值。

在不知道值的情况下,您无法填充每个底层容器,因此您需要寻找获取值的方法。

“解决方案”

您发现了代理模式并意识到它是必要的,因为没有其他方法可以直接获取该值。

您甚至可能会遇到@KenBloom 的一些话,强调需要代理模式,“C++ 没有定义像 Ruby 这样的[]=运算符,像 Z3012DCFF1477E1FEAAB81764587C9 这样的神奇update function 或基本属性,如 Visualized 属性。”

但是,您意识到使用代理模式将破坏 (1) 与原始类型的交互,或 (2) 结构和 class 类型的成员访问。

琐碎用例的答案

这把我们带到了这里。

@NicolBolas 说,“C++ 不允许你做你想做的事情。任何类型的代理类型在某些时候都会表现得不像它正在代理的东西。C++ 代理只能永远是近似值,而不是替代。”

只有第一句话不正确,因为您所要做的就是有条件地继承T

#include <type_traits>  // conditional, is_class
#include <variant>      // monostate

struct myproxy < T, T, false > : public myproxy<T const, T>, <
                                 public std::conditional<std::is_class<T>::value, T, std::monostate>::type
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

这使得mycontainer output 正确地用于琐碎的用例(并解决了原始问题)。

平凡的含义(1)只要mycontainer不被递归使用,或者(2)如果mycontainer被递归使用,那么只要mycontainer仅用于最里面的节点。

如果您使用代理模式并且您的 output 大部分是空的,那么您的用例是不平凡的,获得预期 output 的唯一方法是返回T&但如果您返回T&您不能使用代理模式。

非平凡用例的答案

如前所述,直到 function 返回之后,下标运算符[]才知道它是在读取还是写入值。

您可以尝试找出在 function 返回后执行代码的方法(如果您实际成功,这可能是未定义的行为),或者您可以尝试了解返回T&的含义。

T&返回由 memory 地址支持的引用。

创建变量时会分配 memory 地址。

这意味着我们不需要该值。 我们只需要一个在 function 返回后仍然有效的引用。

一旦 function 返回值将被分配给引用,因此每个接收到引用的底层容器都将具有该值。

您所要做的就是使用不会使迭代器或引用无效的容器作为基本容器。

例如, std::list

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM