是否可以在 C++ 中使用转换迭代器?

Posted

技术标签:

【中文标题】是否可以在 C++ 中使用转换迭代器?【英文标题】:Is it Possible to Have a Transforming Iterator in C++? 【发布时间】:2015-04-01 05:25:49 【问题描述】:

假设我有一个 C++ 迭代器,它不仅遍历数据结构,而且在取消引用时对元素应用转换。

作为一个真实的例子,这里有一个迭代器,它遍历位图中的像素,将位图特定的像素格式转换为方便的结构:

class ConstPixelIterator 

  public: struct Pixel 
    float Red;
    float Green;
    float Blue;
    float Alpha;
  ;

  public: ConstPixelIterator(const Bitmap &bitmap);

  // ...standard iterator functionality...

  public: Pixel operator *() 
    // Read from memory and convert pixel format-specific bytes into Pixel structure
  

;

现在,如果我想实现一个非常量迭代器(即让用户修改像素),最好的方法是什么?

我考虑的一些想法:

我可以将访问器方法放在Pixel 结构中而不是普通字段中,并为其所有者提供电话回家的引用。 然而,这意味着如果用户更改了 R、G、B 和 A,我会将像素转换为位图的像素格式 4 次,并写入内存 4 次。

我可以从迭代器返回一个像素引用,并为它提供一个Update() 方法,如果像素发生更改,则需要调用该方法。 这将是不直观的,并且有用户忘记致电Update 的风险。

我总是可以按值返回Pixel 并提供一个特殊的赋值运算符。 确实打破了标准的迭代器模式 - 分配给迭代器而不取消引用应该移动迭代器,而不是更新它指向的元素

【问题讨论】:

我认为惯用的方式是operartor *() 返回一个(可能是常量)对实际像素的引用。自然,迭代器可以保留对原始容器的引用。 是的,但 实际像素 的格式不同(例如,每像素 16 位,红-绿-蓝为 5-6-5 位)并且我想对用户隐藏这个细节,因此我返回一个代理对象,而不是 实际像素。迭代器当然有对原始容器(Bitmap)的引用——我的麻烦在于告诉迭代器何时需要将代理对象的更改写回原始容器。 看看Boost.Iterator和Boost.Range。 @Cygon 我明白了,让代理包含对原始像素的引用并“刷新”代理销毁时的更改怎么样? 【参考方案1】:

您的 Pixel 结构的成员可以作为属性实现。

这将允许您编写类似

的内容
iter->r = 0;

它不允许你将像素结构发送到函数 希望它是“直截了当”的

在 c++ 中实现属性很容易,例如,请参见 here

多次写入内存应该不是问题,因为它会缓存本地

【讨论】:

【参考方案2】:

我可以将访问器方法放在 Pixel 结构中而不是普通字段中,并为其所有者提供电话回家的引用。然而,这意味着如果用户更改了 R、G、B 和 A,我会将像素转换为位图的像素格式 4 次,并写入内存 4 次。

这推动了 demeter 定律(像素不应该改变位图来设置它自己的值);你不应该这样做。

我可以从迭代器返回一个 Pixel 引用,并为它提供一个 Update() 方法,如果像素发生更改,则需要调用该方法。这将是不直观的,并且有用户忘记调用 Update 的风险。

这是一个有点糟糕的界面,因为在不记住内容的情况下正确使用会很棘手。它也违反了 demeter 的布局(您不应该通过像素更新位图)。

我总是可以按值返回 Pixel 并提供一个特殊的赋值运算符。是否破坏了标准迭代器模式 - 分配给迭代器而不取消引用应该移动迭代器,而不是更新它指向的元素

不要'这样做;这违反了最小意外原则。

考虑改为使像素独立于位图(即像素不知道位图是什么)。

您应该能够轻松地将(有效的)值设置到像素中(使用字段访问器或Update(R, G, B, A) 或类似的)并让位图知道像素是什么以及如何从像素更新自身,使用一个 (或更多)这些:

void Bitmap::Update(int x, int y, const Pixel& p);
void Bitmap::Fill(const Pixel& p);
void Bitmap::SetRange(SomeRangeObject r, const Pixel& p);

这样,对像素的更新操作将需要以下步骤:

从位图中获取像素 根据需要更新/转换像素 从像素更新位图

这不包括谁会知道像素的位置(它们可能不应该是像素本身的一部分),但这是一个开始。

使用此实现,您的相互依赖性保持较低(更新位图像素的功能保留在位图中,而不是像素中),并且迭代模型很简单(与标准迭代器相同)。

【讨论】:

确实,这是许多库中久经考验的真正解决方案。我对迭代器(或类似迭代器)设计的动机是避免对每个像素访问进行完整的边界检查和地址计算。 -- 尽管除非位图实际存储了Pixel 实例,否则我认为迭代模型不会是微不足道的——你仍然会遇到我的问题。除非您的建议是将位图(或其中的一个区域)转换为 Pixel 矩阵,否则请继续处理,然后将其写回。 我在想您可以在迭代器中或在位图的公共接口中构造像素实例。然后像素实例将成为编辑像素的代理/接口。无论哪种方式,我都不确定您是否可以避免边界检查。您是否考虑过将边界检查和转换为像素矩阵作为单独的(可见的)操作来实现?然后使用位图将是这些步骤: 1. 加载位图; 2. 得到像素矩阵; 3. 应用像素变换; 4.将像素矩阵设置为位图。 5. 保存位图 我必须考虑这个:) |提取像素矩阵是可行的,尽管我仍然有点反对这个想法(例如,稀疏更新的开销,需要在工作之前找出感兴趣的区域)。再说一次,它将完全消除代理迭代器问题。 |像素作为代理类似于我的示例#1(只是为了澄清,在我的 3 个示例中,Pixel 不知道Bitmap:它是一个范围有限的辅助类,在最复杂的情​​况下它会保留一个引用到PixelIterator 通知它适当的更新时间)。 投了反对票,因为几十年的图像处理经验已经确定像素和位图的概念在本质上是相互交织的,以至于得墨忒耳定律没有意义。一点也不。像素之于位图就像迭代器之于容器。将类型嵌套为 Bitmap::Pixel 甚至是有意义的。 得墨忒耳法则并没有说反对通过定义的接口访问另一个对象。我认为@utnapistim 的印象是,在我的示例 #1 和 #2 中,PixelIterator 应该处理 Pixel 对象,然后一直引用到 Bitmap 并直接修改它。那将是意大利面设计,德米特指出这一点是正确的:p【参考方案3】:

我们在std::vector<bool>::iterator 中有一个现有示例 - 必须使用一些技巧才能写入单个位。

一种解决方案是返回ProxyPixel。它保留对原始像素的引用。您说更新 R、G、B、A 可能会导致 4 次写入。这是真的,也是可以理解的。在第一次写入 R 之后,底层图像毕竟应该有一个更新的 R 值。

或者您愿意接受最终的更新吗?在这种情况下,您可以延迟回写到ProxyPixel::~ProxyPixel。是的,当代理像素被更改时,底层图像将暂时不同步,但它会更有效。合理的权衡。

【讨论】:

Eric Niebler 最近写了一篇内容丰富的blog post,介绍了与标准迭代器类别有关的代理迭代器问题(只是将其添加为 OP 的 FYI)。 感谢@Björn,感谢他的博文和提到 proxy iterator - 现在我的问题有了一个名称 :) 感谢@MSalters。我完全忘记了std::vector<bool>。这可以提供很好的指导。在其析构函数中更新的ProxyPixel 是另一个有趣的优点和关注点组合(即使我只想写(例如std::fill()),也会首先读取像素。稀疏更新仍然会写入整个位图,而迭代它)。 @Cygon:不要太担心那些多余的读取;只需使函数可内联并让优化器消除它们。

以上是关于是否可以在 C++ 中使用转换迭代器?的主要内容,如果未能解决你的问题,请参考以下文章

通过 C++ 中的 std 迭代器将基类转换为派生

确定 (c++) 迭代器是不是是反向的

C++ STL与迭代器

Java迭代器内容访问

C++迭代器 && vector中迭代器失效

C++迭代器 && vector中迭代器失效