在 C++ 中编码 structarray 映射函子的最清晰方法

发布于 2024-08-03 03:17:56 字数 3313 浏览 3 评论 0原文

这是一项关于最易读的方法的意见调查——是否使用 C++ 成员指针、字节偏移量或模板化函子来定义“从结构 foo 中选择成员 X”。

我有一个包含大型结构向量的类型,并且我正在编写一个实用函数,该函数基本上作为 在某些范围内减少。每个结构将一组因变量与独立维度上的某个点相关联——为了发明一个简化的示例,想象一下它记录了房间随时间变化的一系列环境条件:

// all examples are psuedocode for brevity
struct TricorderReadings
{
  float time;  // independent variable

  float tempurature;
  float lightlevel;
  float windspeed; 
  // etc for about twenty other kinds of data...
}

我的函数只是执行 三次插值来猜测可用样本之间某个给定时间点的条件。

// performs Hermite interpolation between the four samples closest to given time
float TempuratureAtTime( float time, sorted_vector<TricorderReadings> &data)
{
    // assume all the proper bounds checking, etc. is in place
    int idx = FindClosestSampleBefore( time, data );
    return CubicInterp( time, 
                        data[idx-1].time, data[idx-1].tempurature,
                        data[idx+0].time, data[idx+0].tempurature,
                        data[idx+1].time, data[idx+1].tempurature,
                        data[idx+2].time, data[idx+2].tempurature );
}

我想概括这个函数,以便它可以普遍应用于任何成员,而不仅仅是温度。我可以想到三种方法来做到这一点,虽然它们都很容易编码,但我不确定对于一年后必须使用它的人来说什么是最易读的。这就是我正在考虑的:


指向成员的指针语法

typedef int TricorderReadings::* selector;
float ReadingAtTime( time, svec<TricorderReadings> &data, selector whichmember )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, data[idx-1].*whichmember, 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed );

这感觉像是最“C++y”的方式,但它看起来很奇怪,并且整个指向成员的语法很少使用,因此很难理解我团队中的大多数人。这是技术上“正确”的方式,但也是我收到的电子邮件中最困惑的方式。

结构偏移量

float ReadingAtTime( time, svec<TricorderReadings> &data, int memberoffset )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       *(float *) ( ((char *)(&data[idx-1]))+memberoffset ), 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, offsetof(TricorderReadings, windspeed) );

这在功能上与上面相同,但显式地进行指针数学运算。这种方法对于我团队中的每个人(他们在 C++ 之前都学过 C)会立即熟悉和理解,而且它很健壮,但看起来很恶心。

模板化函子

template <class F>
float ReadingAtTime( time, svec<TricorderReadings> &data )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       F::Get(data[idx-1]) ), 
                       /* ...etc */  );
}

// called with:
class WindSelector
{ 
   inline static float Get(const TricorderReadings &d) { return d.windspeed; }
}
ReadingAtTime<WindSelector>( 12.6f, data );

这是最直接、最类似 STL 的做事方式,但它看起来像是一大堆额外的类型和语法以及临时的类定义。它编译成与上面两个几乎完全相同的东西,但它还在可执行文件中转储了一堆冗余函数定义。 (我已使用 /FAcs 验证了这一点,但也许链接器会再次将它们取出。)


以上三个都可以工作,并且编译器会为所有这些发出几乎相同的代码;所以,我必须做出的最重要的选择就是最易读的。你怎么认为?

This is a poll for opinions on the most readable way to do something -- whether to use a C++ pointer-to-member, a byte offset, or a templatized functor to define "select member X from structure foo".

I've got a type that contains a large vector of structures, and I'm writing a utility function that basically operates as a reduce over some range of them. Each structure associates a group of dependent variables with some point along an independent dimension -- to invent a simplified example, imagine that this records a series of environmental conditions for a room over time:

// all examples are psuedocode for brevity
struct TricorderReadings
{
  float time;  // independent variable

  float tempurature;
  float lightlevel;
  float windspeed; 
  // etc for about twenty other kinds of data...
}

My function simply performs a cubic interpolation to guess those conditions for some given point of time in between the available samples.

// performs Hermite interpolation between the four samples closest to given time
float TempuratureAtTime( float time, sorted_vector<TricorderReadings> &data)
{
    // assume all the proper bounds checking, etc. is in place
    int idx = FindClosestSampleBefore( time, data );
    return CubicInterp( time, 
                        data[idx-1].time, data[idx-1].tempurature,
                        data[idx+0].time, data[idx+0].tempurature,
                        data[idx+1].time, data[idx+1].tempurature,
                        data[idx+2].time, data[idx+2].tempurature );
}

I'd like to generalize this function so that it can be applied generically to any member, not just temperature. I can think of three ways to do this, and while they're all straightforward to code, I'm not sure what will be the most readable to whoever has to use this a year from now. Here's what I'm considering:


Pointer-to-member syntax

typedef int TricorderReadings::* selector;
float ReadingAtTime( time, svec<TricorderReadings> &data, selector whichmember )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, data[idx-1].*whichmember, 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed );

This feels like the most "C++y" way to do it, but it looks strange, and the whole pointer-to-member syntax is rarely used and therefore poorly understood by most people on my team. It's the technically "right" way, but also the one I'll get the most confused emails about.

Structure offset

float ReadingAtTime( time, svec<TricorderReadings> &data, int memberoffset )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       *(float *) ( ((char *)(&data[idx-1]))+memberoffset ), 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, offsetof(TricorderReadings, windspeed) );

This is functionally identical to the above, but does the pointer math explicitly. This approach will be immediately familiar and understandable to everyone on my team (who all learned C before C++), and it is robust, but it just seems icky.

Templatized Functor

template <class F>
float ReadingAtTime( time, svec<TricorderReadings> &data )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       F::Get(data[idx-1]) ), 
                       /* ...etc */  );
}

// called with:
class WindSelector
{ 
   inline static float Get(const TricorderReadings &d) { return d.windspeed; }
}
ReadingAtTime<WindSelector>( 12.6f, data );

This is the most straightforward and STL-ish way of doing things, but it seems like a whole bunch of extra typing and syntax and extemporaneous class definitions. It compiles to almost exactly the same thing as the two above, but it also dumps a bunch of redundant function definitions all over the executable. (I've verified this with /FAcs, but maybe the linker takes them out again.)


All the three above will work, and the compiler emits pretty much the same code for all of them; so, the most important choice I have to make is simply which is most readable. What do you think?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

離殇 2024-08-10 03:17:56

我发现在这种情况下模板化函子非常清楚。

ReadingAtTime<WindSelector>( 12.6f, data );

I find the Templatized Functor very clear in this case.

ReadingAtTime<WindSelector>( 12.6f, data );
起风了 2024-08-10 03:17:56

更接近 STL 的方式是使用通用仿函数,它使通过指向成员的指针进行的访问看起来像函数调用。它可能看起来像这样:

#include <functional>

template <class T, class Result>
class member_pointer_t: public std::unary_function<T, Result>
{
    Result T::*member;
public:
    member_pointer_t(Result T::*m): member(m) {}
    Result operator()(const T& o) const { return o.*member; }
};

template <class T, class Result>
member_pointer_t<T, Result> member_pointer(Result T::*member)
{
    return member_pointer_t<T, Result>(member);
}

float ReadingAtTime( float time, const std::vector<TricorderReadings> &data, member_pointer_t<TricorderReadings, float> f )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, f(data[idx-1]));
}

ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed);

该示例还包括一个辅助函数,用于帮助推断仿函数的模板参数(本示例中未使用)。

函数 ReadingAtTime 也可能接受模板化函子:

template <class Func>
float ReadingAtTime( float time, const std::vector<TricorderReadings>& data, Func f);

ReadingAtTime( 12.6f, data, member_pointer(&TricorderReadings::windspeed));

这样您就可以使用各种函数/函子从 data[idx - 1] 获取值,而不仅仅是指向成员的指针。

member_pointer 的更通用等价物可能是 std::tr1::bind 或 std::tr1::mem_fn。

A more STL-ish way would be a generic functor that makes access through a pointer-to-member look like a function call. It might look something like this:

#include <functional>

template <class T, class Result>
class member_pointer_t: public std::unary_function<T, Result>
{
    Result T::*member;
public:
    member_pointer_t(Result T::*m): member(m) {}
    Result operator()(const T& o) const { return o.*member; }
};

template <class T, class Result>
member_pointer_t<T, Result> member_pointer(Result T::*member)
{
    return member_pointer_t<T, Result>(member);
}

float ReadingAtTime( float time, const std::vector<TricorderReadings> &data, member_pointer_t<TricorderReadings, float> f )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, f(data[idx-1]));
}

ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed);

The example also includes a helper function to help deduce template arguments for the functor (not used in this example).

The function ReadingAtTime might also accept a templated functor:

template <class Func>
float ReadingAtTime( float time, const std::vector<TricorderReadings>& data, Func f);

ReadingAtTime( 12.6f, data, member_pointer(&TricorderReadings::windspeed));

This way you could use all kinds of functions / functors to obtain a value from data[idx - 1], not just pointers-to-member.

More generic equivalents of member_pointer might be std::tr1::bind or std::tr1::mem_fn.

遗忘曾经 2024-08-10 03:17:56

如果您的团队由相当聪明的人组成,我想说要相信他们和他们的能力,并使用指向成员语法提供的技术上首选的解决方案。这就是它的用途。

如果您确实担心,可以采取一些步骤来缓解未来的麻烦,方法是

  • 在 typedef 附近的注释中注明,并使用称为“指向成员的指针”语法,以便其他团队成员知道要查找的
  • 内容。它在代码审查中明确指出,其中许多应该存在。如果认为它难以理解或过于晦涩而无法维护,请提出更改。

其他两种方法都有问题,正如您所描述的那样:

  • 两者都需要更多的代码,有更多的打字错误空间等。
  • offsetof原语在它可以应用的类型方面受到限制至:

    由于 C++ 中结构体的扩展功能,在这种语言中,offsetof 的使用仅限于“POD 类型”,对于类来说,或多或少对应于 C 结构体的概念(尽管非派生类具有只有公共非虚拟成员函数并且没有构造函数和/或析构函数也符合 POD 的资格)。

来自此处

If your team is composed of reasonably smart people, I would say to trust them and their abilities, and use the technically preferred solution that pointer-to-member syntax offers. This is the kind of thing it was made for.

If you're really concerned, you can take some steps to ease any future trouble by

  • Noting in a comment near the typedef and the usage that this is called "pointer to member" syntax, so that other team members know what to look up
  • Point it out explicitly in a code review, where many of them should be present. Offer to change it if it's considered incomprehensible or too obscure for maintenance.

The other two approaches have problems, both as you described, and beyond:

  • Both require more code, have more room for typos, etc.
  • The offsetof primitive is limited in what types it can be applied to:

    Because of the extended functionality of structs in C++, in this language, the use of offsetof is restricted to "POD types", which for classes, more or less corresponds to the C concept of struct (although non-derived classes with only public non-virtual member functions and with no constructor and/or destructor would also qualify as POD).

From here.

江湖彼岸 2024-08-10 03:17:56

对于简单的事情,我更喜欢“指向成员的指针”解决方案。然而,函子方法有两个可能的优点:

  1. 将算法与
    数据允许您使用算法
    对于未来更多的事情,正弦
    它可以与任何你提供的东西一起使用
    可以构造一个合适的函子。

  2. 与#1相关,这可能会使
    测试算法更容易,因为您
    有办法提供测试数据
    不涉及的功能
    创建您的完整数据对象
    打算使用。你可以使用更简单的
    模拟对象。

然而,我认为只有当您正在制作的函数非常复杂和/或在许多不同的地方使用时,函子方法才值得。

For simple stuff, I'd prefer the Pointer-to-member solution. However, there are two possible advantages to the functor approach:

  1. separating the algorithm from the
    data allows you to use the algorithm
    for more things in the future, sine
    it work with anything provided you
    can construct a proper functor.

  2. related to #1, this might make
    testing the algorithm easier, as you
    have a way to provide test data to
    the function that does not involve
    creating the full data objects you
    intend to use. You can use simpler
    mock objects.

However, I think the functor approach is worth it only if the function you're making is very complex and/or used in many different places.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文