【问题标题】:Clearest way to code structarray map functor in C++在 C++ 中编写 structarray 映射函子的最清晰方法
【发布时间】:2009-08-28 03:10:43
【问题描述】:

这是一个关于最易读的方法的民意调查——无论是使用 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...
}

我的函数只是执行cubic interpolation 来猜测可用样本之间某个给定时间点的这些条件。

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


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

【问题讨论】:

    标签: c++ readability functor pointer-to-member


    【解决方案1】:

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

    ReadingAtTime<WindSelector>( 12.6f, data );
    

    【讨论】:

      【解决方案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。

      【讨论】:

        【解决方案3】:

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

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

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

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

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

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

        来自here

        【讨论】:

        • 我想我会选择这个。您提出了一个很好的观点,即任何值得他的薪水的开发人员都应该能够通过阅读评论来学习一些语法;而且我认为如果我强迫他们在每次他们想要插入变量时即时构建一个小类,我会面临团队的武装叛乱。
        【解决方案4】:

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

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

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

        但是,我认为函子方法只有在您创建的函数非常复杂和/或在许多不同地方使用时才值得。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-12-13
          • 2012-04-03
          • 1970-01-01
          • 2010-12-09
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多