3

这是一个关于最易读的方式做某事的民意调查——是使用指向成员的 C++ 指针、字节偏移量还是模板化仿函数来定义“从结构 foo 中选择成员 X”。

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

// 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验证了这一点,但也许链接器会再次将它们取出。)


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

4

4 回答 4

3

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

ReadingAtTime<WindSelector>( 12.6f, data );
于 2009-08-28T03:21:09.117 回答
2

一种更类似于 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。

于 2009-08-28T09:58:12.800 回答
1

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

如果你真的很担心,你可以采取一些措施来缓解未来的麻烦:

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

如您所描述的,其他两种方法都存在问题,并且超出:

  • 两者都需要更多的代码,有更多的错别字空间等。
  • offsetof语的适用类型受到限制:

    由于 C++ 中 struct 的扩展功能,在这种语言中,offsetof 的使用仅限于“POD 类型”,对于类来说,或多或少对应于 C 的 struct 概念(尽管非派生类只有 public non -virtual 成员函数并且没有构造函数和/或析构函数也可以作为 POD)。

这里

于 2009-08-28T03:22:54.870 回答
1

对于简单的东西,我更喜欢 Pointer-to-member 解决方案。但是,仿函数方法有两个可能的优点:

  1. 将算法与数据分开允许您在未来将算法用于更多事情,因为它可以与任何可以构造适当函子的东西一起使用。

  2. 与#1 相关,这可能会使测试算法更容易,因为您可以向函数提供测试数据,而无需创建您打算使用的完整数据对象。您可以使用更简单的模拟对象。

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

于 2009-08-28T04:06:59.227 回答