在不中断与原始类型的交互和其他类型的成员访问的情况下,如何在使用代理模式时区分读取和写入?
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 的code作为基础。
#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
输出A
Writing.
Reading.
Reading.
Writing.
评论一
现在添加这个 class:
class myclass
{
public:
void xyzzy()
{
std::cout << "Xyzzy." << std::endl;
}
};
并在调用 xyzzy
测试成员访问时相应地更改主函数:
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();
}
输出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
^^^
输出C
Writing.
Reading.
Reading.
Writing.
Xyzzy.
评论 C
然而,当我们切换回原始类型时,无条件继承 T
会导致不同的编译失败。
输出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
有条件地继承 T
结构和 class 类型,但我对 C++ 不够熟练,不知道这是否会导致不同的潜在问题。
经过一周的调查和审查数十个代理模式实现后,我发现几乎每个代理模式实现都被破坏,因为主要运算符方法的编写方式。
例证:
myproxy<T> operator[] (typename std::vector<T>::size_type const i)
^^^^^^^
这应该是T
。显然,T<T>
在这里不起作用,但 T
可以。
实际上这应该是 T&
(以避免细微的破损,尤其是当我们使用地图或类似地图的容器作为底层时)但这在这里不起作用要么不重写实现。
但是无论我们使用 T
还是 T&
我们都会得到:
输出E
Reading.
Reading.
Reading.
Reading.
Reading.
Xyzzy.
评论E
如您所见,我们失去了区分读取和写入的能力。
此外,当我们切换回基本类型时,此方法会导致不同的编译失败:
输出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
或 map-like 下标运算符 []
.
问题
您意识到,当您使用下标运算符 []
插入值时,每个底层容器也必须接收该值。但是,您发现下标运算符 []
直到函数 returned.
后才知道它是在读取还是写入值
在不知道该值的情况下,您无法填充每个底层容器,因此您需要寻找获取该值的方法。
“解决方案”
您发现代理模式并意识到它是必要的,因为没有其他方法可以直接获取该值。
你甚至可能会遇到@KenBloom 的 some words,它强调代理模式的必要性,“C++ 没有定义像 Ruby 这样的 []=
运算符,一个神奇的 update
像 Scala 那样的函数,或者像 Visual Basic 这样的参数化属性。"
但是,您意识到使用代理模式会破坏 (1) 与基本类型的交互,或 (2) 结构和 class 类型的成员访问。
简单用例的答案
这让我们来到这里。
@NicolBolas 说,“C++ 不允许你做你想做的事情。任何一种代理类型在某些时候都不会表现得像它正在代理的东西。A 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
对于普通用例正确输出(并解决了原始问题)。
琐碎的含义 (1) 只要 mycontainer
不被递归使用,或 (2) 如果 mycontainer
被递归使用,那么只要 mycontainer
仅用于最里面的节点。
如果您使用代理模式并且您的输出大部分为空,那么您的用例是 non-trivial 并且获得预期输出的唯一方法是 return T&
但是如果你 return T&
你不能使用代理模式。
non-Trivial 个用例的答案
如前所述,下标运算符 []
直到函数 returned 后才知道它是在读取还是写入值。
您可以尝试找出一种在函数 returned 后执行代码的方法(如果您真的成功了,这可能是未定义的行为)或者您可以尝试理解什么 return ing T&
表示.
T&
return 由内存地址支持的引用。
创建变量时分配内存地址。
这意味着我们不需要这个值。我们只需要一个在函数 returns.
之后仍然有效的引用
一旦函数 returns 值将被分配给引用,因此接收引用的每个底层容器都将具有该值。
您所要做的就是使用不会使迭代器或引用失效的容器作为基础容器。
例如,std::list
.
前言
在调查和审查了数十种代理模式实施一周之后,我提出了这个问题。
请不要错误地将此问题标记为重复问题,除非答案不会破坏 (1) 结构和 class 类型的成员访问以及 (2) 与原始类型的交互。
代码
对于我最小的、可重现的例子,我使用来自@Pixelchemist 的code作为基础。
#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
输出A
Writing.
Reading.
Reading.
Writing.
评论一
现在添加这个 class:
class myclass
{
public:
void xyzzy()
{
std::cout << "Xyzzy." << std::endl;
}
};
并在调用 xyzzy
测试成员访问时相应地更改主函数:
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();
}
输出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
^^^
输出C
Writing.
Reading.
Reading.
Writing.
Xyzzy.
评论 C
然而,当我们切换回原始类型时,无条件继承 T
会导致不同的编译失败。
输出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
有条件地继承 T
结构和 class 类型,但我对 C++ 不够熟练,不知道这是否会导致不同的潜在问题。
经过一周的调查和审查数十个代理模式实现后,我发现几乎每个代理模式实现都被破坏,因为主要运算符方法的编写方式。
例证:
myproxy<T> operator[] (typename std::vector<T>::size_type const i)
^^^^^^^
这应该是
T
。显然,T<T>
在这里不起作用,但T
可以。实际上这应该是
T&
(以避免细微的破损,尤其是当我们使用地图或类似地图的容器作为底层时)但这在这里不起作用要么不重写实现。
但是无论我们使用 T
还是 T&
我们都会得到:
输出E
Reading.
Reading.
Reading.
Reading.
Reading.
Xyzzy.
评论E
如您所见,我们失去了区分读取和写入的能力。
此外,当我们切换回基本类型时,此方法会导致不同的编译失败:
输出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
或 map-like 下标运算符 []
.
问题
您意识到,当您使用下标运算符 []
插入值时,每个底层容器也必须接收该值。但是,您发现下标运算符 []
直到函数 returned.
在不知道该值的情况下,您无法填充每个底层容器,因此您需要寻找获取该值的方法。
“解决方案”
您发现代理模式并意识到它是必要的,因为没有其他方法可以直接获取该值。
你甚至可能会遇到@KenBloom 的 some words,它强调代理模式的必要性,“C++ 没有定义像 Ruby 这样的 []=
运算符,一个神奇的 update
像 Scala 那样的函数,或者像 Visual Basic 这样的参数化属性。"
但是,您意识到使用代理模式会破坏 (1) 与基本类型的交互,或 (2) 结构和 class 类型的成员访问。
简单用例的答案
这让我们来到这里。
@NicolBolas 说,“C++ 不允许你做你想做的事情。任何一种代理类型在某些时候都不会表现得像它正在代理的东西。A 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
对于普通用例正确输出(并解决了原始问题)。
琐碎的含义 (1) 只要 mycontainer
不被递归使用,或 (2) 如果 mycontainer
被递归使用,那么只要 mycontainer
仅用于最里面的节点。
如果您使用代理模式并且您的输出大部分为空,那么您的用例是 non-trivial 并且获得预期输出的唯一方法是 return T&
但是如果你 return T&
你不能使用代理模式。
non-Trivial 个用例的答案
如前所述,下标运算符 []
直到函数 returned 后才知道它是在读取还是写入值。
您可以尝试找出一种在函数 returned 后执行代码的方法(如果您真的成功了,这可能是未定义的行为)或者您可以尝试理解什么 return ing T&
表示.
T&
return 由内存地址支持的引用。
创建变量时分配内存地址。
这意味着我们不需要这个值。我们只需要一个在函数 returns.
之后仍然有效的引用一旦函数 returns 值将被分配给引用,因此接收引用的每个底层容器都将具有该值。
您所要做的就是使用不会使迭代器或引用失效的容器作为基础容器。
例如,std::list
.