背景

上周想着实现一个跟踪元素修改的容器,背景是调试用:输入的数据可能有很多,但最后给下游送出的只有其中一部分,于是需要跟踪一下哪些元素、因为什么原因在过程中被“移除”了。

第一反应是,跟踪这个动作非常重要,所以为了避免遗漏记录,所以有必要统一接口来保证元素的一致性。

1
2
3
4
5
6
// bad
erase(id);
log(id);

// good
erase_and_log(id);

也正是这个第一印象,导致后面的大翻车。

整理需求

于是需求很简单:

  1. 容器要能跟踪“删除”事件;
  2. 被“删除”的元素实际并不会消失,在一定的业务周期(循环)中还能访问:主业务流程只关心剩下的有效数据,而调试流程关心数据全集,所以必须要能访问两种不同类型的数据;

开撸代码

简单翻译一下需求,立马就有想法了:
创建一个新的类,内部持有一个标准库的数据容器,并且提供类似 stl 的 API,book keeping 相关的逻辑就在操作容器前/后实现。

然后推演一下需求的实现:
erase 的时候并不删除,而是通过标志位记录数据无效;
为了这个功能,引入了这些配套的设施:

  1. 需要用 std::pair 或者 struct 来包装 T databool is_valid,来对应记录元素的有效性;
  2. 提供 visit_all visit_valid,访问不同有效性的数据;

于是各种问题就开始出现了:

  1. 因为用了组合,为了提供类似 stl 的 API,需要提供各种接口和重载,如 push_back(const T&) push_back(T&&)
  2. 需要重写 operator[]iterator,并且 iterator 的解引用还不能暴露 bool is_valid 标记位;
  3. 所有能遍历内容相关的接口(如 2. 中),都要考虑定义“提供的到底是所有元素,还是有效元素”,把复杂度暴露给了使用者;
  4. 为了区分访问元素,需要提供 visit_all visit_valid,用起来很不方便;

评价

综上,这个看似简单的需求和方案,对开发者和使用者都很不友好:

  1. 对业务主流程使用者:
    • 类的接口可能没有 stl 丰富;
    • 明明只需要简单的删除元素操作,被迫引入“无效元素”的概念,访问元素的逻辑也很不舒服;
  2. 对类的开发者:
    • 重复实现一些无用的接口(骂你呢傻子 C++ 的 iterator const 什么鬼的);

简评:垃圾。

感恩 GPT 助我反思

其实在推演过程中就已经举步维艰了,但因为有 GPT 加持,让我很顺利地“过关斩将”,无痛量产了很多狗屎冗余代码,得以让功能诞生了。但是最后我重新冷静下来审视这个接口和调用方式,发现“创新”出这个机制除了浪费时间一无是处,倒是完全符合对 C++ 程序员造无用轮子的刻板印象。

那最后怎么解决的?数了一下,其实要跟踪的调用就两三处,用一个 std::vector 把操作记录存下来,最后合并一下好了,元素和跟踪信息的一致性,在这里维护其实也没这么复杂。

经典过度设计(气)。