简体   繁体   English

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

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

Preface前言

I'm asking this question after a week of investigating and reviewing dozens and dozens of proxy pattern implementations.经过一周的调查和审查数十个代理模式实现后,我提出了这个问题。

Please, do not incorrectly flag this question as a duplicate unless the answer does not break (1) member access for structs & class types and (2) interaction with primitive types.请不要错误地将这个问题标记为重复问题,除非答案不会破坏 (1) 结构和 class 类型的成员访问以及 (2) 与原始类型的交互。

Code代码

For my Minimal, Reproducible Example I'm using code from @Pixelchemist as the base.对于我最小的、可重现的示例,我使用来自 @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];
}

Compile Command编译命令

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

Execution Command执行命令

./proxy

Output A Output A

Writing.
Reading.
Reading.
Writing.

Comment A评论 A

Now add this class:现在添加这个 class:

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

and change the main function accordingly while calling xyzzy to test member access:并在调用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 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();
      |           ^~~~~

Comment B评论 B

One way to resolve this is to unconditionally inherit T .解决此问题的一种方法是无条件继承T

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

Output C Output C

Writing.
Reading.
Reading.
Writing.
Xyzzy.

Comment C评论 C

However, unconditionally inheriting T causes a different compile failure when we switch back to primitive types.但是,当我们切换回原始类型时,无条件继承T会导致不同的编译失败。

Output D 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
      |        ^~~~~~~~~~~~~~~~~~~~~~~

Comment D评论 D

We can probably conditionally inherit T for structs and class types using std::enable_if but I'm not proficient enough with C++ to know if this causes different underlying issues.我们可能可以使用std::enable_if有条件地为结构和 class 类型继承T但我对 C++ 不够熟练,无法知道这是否会导致不同的潜在问题。

After a week of investigating and reviewing dozens and dozens of proxy pattern implementations I have discovered that almost every proxy pattern implementation is broken because of how the primary operator method(s) are written.经过一周的调查和审查几十个代理模式实现后,我发现几乎每个代理模式实现都被破坏了,因为主要操作符方法的编写方式。

Case in point:一个例子:

myproxy<T> operator[] (typename std::vector<T>::size_type const i)
^^^^^^^
  1. This should be T .这应该是T Obviously, T<T> doesn't work here but T does.显然, T<T>在这里不起作用,但T起作用。

  2. In fact this should specifically be T& (to avoid subtle breakage, especially if we are using a map or map-like container as the underlying) but that doesn't work here either without rewriting the implementation.事实上,这应该是T& (为了避免细微的损坏,特别是如果我们使用 map 或类似地图的容器作为底层),但如果不重写实现,这在这里也不起作用。

But regardless of whether we use T or T& we'll get:但无论我们使用T还是T&我们都会得到:

Output E Output E

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

Comment E评论 E

As you can see, we lost the ability to distinguish reads from writes.如您所见,我们失去了区分读取和写入的能力。

Additionally, this method causes a different compile failure when we switch back to primitive types:此外,当我们切换回原始类型时,此方法会导致不同的编译失败:

Output F 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];
      |

Comment F评论 F

We can probably resolve this by adding another class to access the components as lvalues but I'm also not proficient enough with C++ to know if this causes different underlying issues.我们可以通过添加另一个 class 来以左值访问组件来解决此问题,但我对 C++ 也不够熟练,无法知道这是否会导致不同的潜在问题。

Question问题

How do we distinguish reads from writes when using the proxy pattern without breaking (1) interaction with primitive types, and (2) member access for structs & class types?在使用代理模式时,我们如何区分读取和写入而不破坏(1)与原始类型的交互,以及(2)结构和 class 类型的成员访问?

There's no short answer to this one so if you don't understand the problem then start from the beginning otherwise start with Answer for Trivial Use Cases which addresses the original question.这个问题没有简短的答案,所以如果您不理解问题,请从头开始,否则从解决原始问题的简单用例答案开始。

Premise前提

You create a wrapper around two or more containers and want to support the std::map or map-like subscript operator [] .您围绕两个或更多容器创建包装器并希望支持std::map或类似地图的下标运算符[]

Problem问题

You realize that when you insert values using the subscript operator [] that every underlying container must also receive this value.您意识到,当您使用下标运算符[]插入值时,每个底层容器也必须接收该值。 However, you discover that the subscript operator [] doesn't know if it's reading or writing a value until after the function has returned.但是,您发现下标运算符[]直到 function 返回之后才知道它是在读取还是写入值。

Without knowing the value you can't populate every underlying container so you search for ways to obtain the value.在不知道值的情况下,您无法填充每个底层容器,因此您需要寻找获取值的方法。

"Solution" “解决方案”

You discover the proxy pattern and realize it's necessary since there is no other way to directly obtain the value.您发现了代理模式并意识到它是必要的,因为没有其他方法可以直接获取该值。

You may even encounter some words by @KenBloom which emphasize the need for the proxy pattern, "C++ doesn't define a []= operator like Ruby, a magic update function like Scala, or parameterized properties like Visual Basic."您甚至可能会遇到@KenBloom 的一些话,强调需要代理模式,“C++ 没有定义像 Ruby 这样的[]=运算符,像 Z3012DCFF1477E1FEAAB81764587C9 这样的神奇update function 或基本属性,如 Visualized 属性。”

However, you realize that use of the proxy pattern will break either (1) interaction with primitive types, or (2) member access for structs & class types.但是,您意识到使用代理模式将破坏 (1) 与原始类型的交互,或 (2) 结构和 class 类型的成员访问。

Answer for Trivial Use Cases琐碎用例的答案

Which brings us here.这把我们带到了这里。

@NicolBolas said, "C++ doesn't allow you to do the kind of thing you want to do. Any kind of proxy type is going to, at some point, not behave like the thing it is proxying. A C++ proxy can only ever be an approximation, not a replacement." @NicolBolas 说,“C++ 不允许你做你想做的事情。任何类型的代理类型在某些时候都会表现得不像它正在代理的东西。C++ 代理只能永远是近似值,而不是替代。”

Only the first sentence is not true since all you have to do is conditionally inherit T .只有第一句话不正确,因为您所要做的就是有条件地继承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
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This makes mycontainer output correctly for trivial use cases (and addresses the original question).这使得mycontainer output 正确地用于琐碎的用例(并解决了原始问题)。

Trival meaning (1) as long as mycontainer is not used recursively, or (2) if mycontainer is used recursively then as long as mycontainer is only used for the innermost node.平凡的含义(1)只要mycontainer不被递归使用,或者(2)如果mycontainer被递归使用,那么只要mycontainer仅用于最里面的节点。

If you use the proxy pattern and your output is mostly empty then your use case is non-trivial and the only way to get the expected output is to return T& but if you return T& you cannot use the proxy pattern.如果您使用代理模式并且您的 output 大部分是空的,那么您的用例是不平凡的,获得预期 output 的唯一方法是返回T&但如果您返回T&您不能使用代理模式。

Answer for non-Trivial Use Cases非平凡用例的答案

As previously mentioned, the subscript operator [] doesn't know if it's reading or writing a value until after the function has returned.如前所述,直到 function 返回之后,下标运算符[]才知道它是在读取还是写入值。

You can try to figure out a way to execute code after the function has returned (which would probably be undefined behavior if you actually succeeded) or you can try to understand what returning T& means.您可以尝试找出在 function 返回后执行代码的方法(如果您实际成功,这可能是未定义的行为),或者您可以尝试了解返回T&的含义。

T& returns a reference backed by a memory address. T&返回由 memory 地址支持的引用。

A memory address is assigned when a variable is created.创建变量时会分配 memory 地址。

This means we don't need the value.这意味着我们不需要该值。 We only need a reference that will remain valid after the function returns.我们只需要一个在 function 返回后仍然有效的引用。

Once the function returns the value will be assigned to the reference and therefore every underlying container that received the reference will have the value.一旦 function 返回值将被分配给引用,因此每个接收到引用的底层容器都将具有该值。

All you have to do is use a container that does not invalidate iterators or references as the base container.您所要做的就是使用不会使迭代器或引用无效的容器作为基本容器。

For instance, std::list .例如, std::list

暂无
暂无

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

相关问题 如何区分数据结构和抽象数据类型? - How do I distinguish between data structures and abstract data types? 如何让这个模板类编译为类和基元类型? - How do I get this template class to compile for Classes and primitive types? 如何复制包含非原始类型的数组? - How do I copy arrays that contain non primitive types? 如何强类型定义非基本类型? - How do I strongly typedef non-primitive types? 通过包装器从 C 访问 C++ API 时,如何访问枚举类型? - When accessing C++ API from C via a wrapper, how do I access enum types? 如何使用C ++中的好友函数将成员变量从一个类访问到另一个类? - How do I access member variables from one class into other using friend functions in C++? 当从函数返回为“const”时,为什么原始类型和用户定义类型的行为不同? - Why do primitive and user-defined types act differently when returned as 'const' from a function? 如何设计具有不同类型通知和观察者的观察者模式? - How do I design an Observer pattern with different types of notifications and observers? 一个线程写入变量,另一个线程读取变量我如何(pre-C ++ 11)保护该变量? - One thread writes to a variable the other thread reads the variable how do I (pre-C++11) protect that variable? 如何使用 GraalVM 从 C++ 调用具有非原始类型作为参数的 Java 入口点方法 - How to call a Java entrypoint method with non-primitive types as parameters from C++ using GraalVM
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM