从一个结构体向量中,获取一个向量,该向量收集每个结构体的一个字段。

14

假设我有以下结构体:

struct Point {
    double X,Y,Z;
};

以及以下向量:

std::vector<Point> v;
// populate v with random points

现在,我想调用类似 collect(v, X) 的函数,并得到一个包含原始结构体向量中的 X 值的 std::vector,例如:

v.push_back(Point{1.0, 2.0,  3.0});
v.push_back(Point{1.1, 0.0, -0.5});
auto ans = collect(v,X);
// ans = [1.0, 1.1]

我认为这是一个非常常见的任务,我相信有一个好的名字,但我在提问时想不出来(请随意指出!)。

我可以做到这一点:

std::vector<double> collectX(std::vector<Point> v) {
    std::vector<double> output;
    for (auto elem : v) {
        output.push_back(elem.X);
    }
}
/* Repeat for each field the struct Point has... */

我知道C++没有反射。我想知道是否有解决方法?你可以想象,我正在处理的结构体不仅有3个字段,因此为每个字段编写一个方法有些令人望而生畏和不优雅。

6个回答

8

因此为每个字段编写一个方法有些令人望而生畏和不优雅。

一个可行的解决办法是将字段标识符也作为参数传递。

std::vector<double> collect(double Point::* f, std::vector<Point> const& v) {
    std::vector<double> output;
    for (auto const& elem : v) {
        output.push_back(elem.*f);
    }
    return output;
}

要这样称呼:

collect(&Point::X, v);

如果类型不总是 double,那么以上内容可以很容易地将其作为成员类型的模板:

template<typename T>
std::vector<T> collect(T Point::* f, std::vector<Point> const& v) {
    std::vector<T> output;
    for (auto const& elem : v) {
        output.push_back(elem.*f);
    }
    return output;
}

最后,你要找的这种提取术语是“投影”。大致来说,当将函数投影到轴上时,就会得到这样的结果。在我们的例子中,该函数将向量的索引映射到“点”,而投影则是指在“x”轴上的投影。
它也可以使用C++标准库或ranges-v3库动态编写。投影是对项目范围进行的非常常见的操作,因此许多以范围为中心的库都具有执行此操作的功能。

太酷了!虽然在 SO 上找不到 "projection" 这个术语。 - imbr
现在我也发现这个链接很有用。 - imbr

5
使用 std::transformstd::back_inserterstd::mem_fn
#include <functional>
//...
std::vector<Point> v{{0,1,2},{9,8,7}};

std::vector<double> x;

std::transform(v.begin(), v.end(), std::back_inserter(x),
           std::mem_fn(&Point::x));

编译器通常可以优化掉std::mem_fn背后的间接引用。


1
transform is a member of #include <algorithm>. #include <functional> is needed for std::mem_fn - y.selivonchyk

3
你可以使用std::transformstd::back_inserter来实现。
std::vector<Point> v;
v.push_back(Point{1.0, 2.0,  3.0});
v.push_back(Point{1.1, 0.0, -0.5});

std::vector<double> x;

std::transform(v.begin(), v.end(), std::back_inserter(x),
               [](Point const& p) -> double { return p.x; });

1

一个 range-v3 + 宏解决方案,可用于所有领域和类型:

#define view_extract_field(fname) (view::transform([](const auto& val) { return val.fname; }))

auto vx = v | view_extract_field(X) | to_vector;

2
auto vx = v | view::transform(&Point::x); 是很好的。 (顺便说一下,to_vector 部分并不总是必需的,一旦我们使用范围,仍然保持“懒惰”)。 - Jarod42

1
您可以为这种东西使用模板。
template<typename C, typename F>
auto vmap(F f, const C& c) -> std::vector<decltype(f(*c.begin()))> {
    std::vector<decltype(f(*c.begin()))> res;
    for (auto& x : c) res.push_back(f(x));
    return res;
}

用作

auto x_values = vmap([](const Point& p){ return p.x; }, pts);

vmap(f, c)返回一个std::vector,其中包含对c的元素应用f后的任何结果,而c是任何标准容器。

为了提取x,我使用一个lambda表达式[](const Point& p){ return p.x; }作为f


0

这并没有直接回答你的问题。如果你关心性能,并且需要经常执行此拆分操作,那么考虑不同的存储设计可能会有所帮助。

class Points {
  public:
    //Constructor ...
    std::vector<double> & getX() const;
  private:
    std::vector<double> x; 
    std::vector<double> y;
    std::vector<double> z;
};

std::vector<double> & Points::getX() {
    return x;
}

以这种方式,您无需复制数据点的x值(可能需要大量内存),并且可以快速访问它们。另一方面,一个数据点的缓存局部性将变差。因此,如果您遇到性能问题,请测量您的数据点,然后再考虑此想法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接