c++ - C++ 中数据聚合和后处理的习惯用法

标签 c++

编程中的一项常见任务是动态处理数据,并在收集完所有数据后进行一些后期处理。一个简单的例子就是计算平均值(和其他统计数据),您可以在其中创建这样的类

class Statistic {
public:
  Statistic() : nr(0), sum(0.0), avg(0.0) {}

  void add(double x) { sum += x; ++nr; }
  void process() { avg = sum / nr; }

private:
  int nr;
  double sum;
  double avg;
};

这种方法的一个缺点是,我们总是必须记住在添加所有数据后调用 process() 函数。由于在 C++ 中我们有类似 RAII 的东西,这似乎不是一个理想的解决方案。

以Ruby为例,我们可以这样写代码

class Avg
  attr_reader :avg

  def initialize
    @nr = 0
    @sum = 0.0
    @avg = nil
    if block_given?
      yield self
      process
    end
  end

  def add(x)
    @nr += 1
    @sum += x.to_f
  end

  def process
    @avg = @sum / @nr
  end
end

然后我们可以这样调用

avg = Avg.new do |a|
  data.each {|x| a.add(x)}
end

并且 process 方法在退出 block 时自动调用。

C++ 中是否有可以提供类似功能的习语?

澄清一下:这个问题不是关于计算平均值的。它是关于以下模式:将数据提供给对象,然后,当所有数据都提供时,触发处理步骤。我对基于上下文的方法自动触发处理步骤很感兴趣——或者这在 C++ 中不是一个好主意的原因。

最佳答案

“惯用平均数”

我不懂 Ruby,但你不能直接翻译成语。我知道计算平均值只是一个示例,所以让我们看看我们可以从该示例中得到什么...

计算总和和容器中元素平均值的惯用方法是 std::accumulate:

std::vector<double> data;
// ... fill data ...
auto sum = std::accumulate( a.begin(), a.end() , 0.0);
auto avg = sum / a.size();

构建 block 是容器、迭代器和算法。

如果容器中没有现成可用的要处理的元素,您仍然可以使用相同的算法,因为算法只关心迭代器。编写自己的迭代器需要一些样板文件。以下只是一个玩具示例,计算调用同一函数一定次数的结果的平均值:

#include <numeric>

template <typename F>
struct my_iter {
    F f;
    size_t count;
    my_iter(size_t count, F f) : count(count),f(f) {}
    my_iter& operator++() {
        --count;
        return *this;
    }
    auto operator*() { return f(); }
    bool operator==(const my_iter& other) const { return count == other.count;}
};


int main()
{
    auto f = [](){return 1.;};
    auto begin = my_iter{5,f};
    auto end = my_iter{0,f};
    auto sum = std::accumulate( begin, end, 0.0);
    auto avg = sum / 5;
    std::cout << sum << " " << avg;
}

输出是:

5 1

假设您有一个要调用的函数的参数 vector ,那么调用 std::accumulate 是直截了当的:

#include <iostream>
#include <vector>
#include <numeric>

int main()
{
    auto f = [](int x){return x;};
    std::vector<int> v = {1,2,5,10};
    
    auto sum = std::accumulate( v.begin(), v.end(), 0.0, [f](int accu,int add) {
        return accu + f(add);
    });
    auto avg = sum / 5;
    std::cout << sum << " " << avg;
}

std::accumulate 的最后一个参数指定元素如何相加。我没有直接将它们相加,而是将调用函数的结果相加。输出是:

18 3.6

针对您的实际问题

从字面上看您的问题并回答 RAII 部分,这是您可以在统计类中使用 RAII 的一种方法:

struct StatisticCollector {
private:
    Statistic& s;
public:
    StatisticCollector(Statistic& s) : s(s) {}
    ~StatisticCollector() { s.process(); }
};

int main()
{
    Statistic stat;
    {
        StatisticCollector sc{stat};
        //for (...)
        // stat.add( x );
    } // <- destructor is called here

}

PS: 最后但并非最不重要的一点是,还有一种方法可以保持简单。您的类定义有点破损,因为所有结果都是私有(private)的。一旦你解决了这个问题,很明显你不需要 RAII 来确保 process 被调用:

class Statistic {
public:
  Statistic() : nr(0), sum(0.0), avg(0.0) {}

  void add(double x) { sum += x; ++nr; }
  double process() { return sum / nr; }
private:
  int nr;
  double sum;
};

在我看来,这是正确的界面。用户不能忘记调用 process 因为要获得结果,他们需要调用它。如果该类的唯一目的是累积数字和处理结果,则不应封装结果。结果供类的用户存储。

关于c++ - C++ 中数据聚合和后处理的习惯用法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64050045/

相关文章:

C++ new 分配比预期更多的空间

c++ - 你如何获得 shared_ptr 弱计数?

c++ - 实现非成员 IO 操作符

c++ - 如何在 postgresql 的 c++ 中使用 pqxx 在 sql 查询中执行 IN?

c++ - 如何将 QString 视为文件位置并获取其目录

c++ - C++ 中有没有办法将给定类的函数限制为仅另一个类(不使用继承, friend )?

c++ - c/c++ 自然语言处理库

c++ - "rename"禁止复制文件

c++ - 使用 QSettings 保存 QSpinBox 和 QComboBox 的组合

C++:添加逻辑上正确的 if 语句会使我的程序崩溃