【问题标题】:Boost::Tuples vs Structs for return valuesBoost::Tuples vs Structs 返回值
【发布时间】:2010-09-29 10:02:06
【问题描述】:

我正试图了解元组(感谢@litb),使用它们的常见建议是返回 > 1 值的函数。

这是我通常会使用结构的东西,在这种情况下我无法理解元组的优势 - 对于最终懒惰的人来说,这似乎是一种容易出错的方法。

Borrowing an example,我会用这个

struct divide_result {
    int quotient;
    int remainder;
};

使用元组,您将拥有

typedef boost::tuple<int, int> divide_result;

但是如果不阅读您正在调用的函数的代码(或 cmets,如果您愚蠢到相信它们),您将不知道哪个 int 是商,反之亦然。好像有点……

struct divide_result {
    int results[2]; // 0 is quotient, 1 is remainder, I think
};

...这不会让我充满信心。

那么,元组相对于弥补歧义的结构的优势是什么

【问题讨论】:

  • 好问题......从答案看来,使用元组的唯一原因是懒惰......并不是说懒惰偶尔没有它的位置,但我不想这样做在生产代码中。
  • 这与输入参数常见的惰性相同:参数的顺序很重要,除非它们都有不兼容的类型,在这种情况下你不会弄错。返回元组也是如此。我们可以为每个函数的参数定义一个结构,但是我们太懒了。

标签: c++ tuples boost-tuples


【解决方案1】:

元组

我想我同意你的观点,即什么位置对应什么变量的问题会引起混淆。但我认为有两个方面。一个是call-side,一个是callee-side

int remainder; 
int quotient;
tie(quotient, remainder) = div(10, 3);

我认为我们得到的结果非常清楚,但如果您必须一次返回更多值,它可能会变得混乱。一旦调用者的程序员查阅了div 的文档,他就会知道什么位置是什么,并且可以编写有效的代码。根据经验,我会说不要一次返回超过 4 个值。除此之外,更喜欢结构。

输出参数

当然也可以使用输出参数:

int remainder; 
int quotient;
div(10, 3, &quotient, &remainder);

现在我认为这说明了元组如何优于输出参数。我们将div 的输入与其输出混合在一起,但没有获得任何优势。更糟糕的是,我们让该代码的读者对div实际返回值可能是什么表示怀疑。当输出参数有用时,有 个很好的例子。在我看来,只有在没有其他方法的情况下才应该使用它们,因为返回值已经被采用并且不能更改为元组或结构。 operator&gt;&gt; 是使用输出参数的一个很好的例子,因为返回值已经为流保留,所以你可以链接 operator&gt;&gt; 调用。如果您与运算符无关,并且上下文不是很清楚,我建议您使用指针,在调用端发出信号表明该对象实际上被用作输出参数,除了适当的 cmets。

返回一个结构

第三种选择是使用结构体:

div_result d = div(10, 3);

我认为这肯定会因清晰而获奖。但请注意,您仍然必须访问该结构中的结果,并且结果不会“裸露”在桌子上,就像输出参数和与 tie 一起使用的元组一样。

我认为这些天来的一个重点是让一切尽可能通用。所以,假设你有一个可以打印元组的函数。你可以这样做

cout << div(10, 3);

并显示您的结果。我认为另一方面,元组显然因其多功能性质而获胜。使用 div_result 执行此操作,您需要重载 operator

【讨论】:

  • 使用 out params 方法编写调用者代码要容易得多——intellisense 可以为您提供帮助。此外,按照惯例,我们定义了一个名为“OUT”的空宏并将其放在 out 参数的前面。
  • 智能感知是我认为的一个好点。但我使用的 IDE 没有这个功能,我认为理想情况下函数参数应该用于输入,输出的返回值(而不是未使用的,如该示例)仍然有效
  • 即使您没有智能感知,手动执行智能感知所做的工作也容易得多——查看方法声明 :-) 我同意在一个上输入在视觉上更有吸引力一方面和输出另一方面,但这只是保持一致并习惯它的问题。
  • “领带”的 intent 非常清楚,但没有编译时检查您的项目是否按正确的顺序排列。所以每次调用 div() 都是一个潜在的错误点。我的方法是“代码说明了您的意思,而 cmets 解释了原因”。使用元组,代码还不够。
  • 是的,我很害怕。我也看到了这个问题。我认为只有当你能记住那个顺序时才应该使用元组。当然,您必须至少查看一次其文档的标题(与往常一样),但是您知道顺序。对于 div,顺序很容易记住。对于像 [...] 这样的东西
【解决方案2】:

另一种选择是使用 Boost Fusion 映射(代码未经测试):

struct quotient;
struct remainder;

using boost::fusion::map;
using boost::fusion::pair;

typedef map<
    pair< quotient, int >,
    pair< remainder, int >
> div_result;

您可以相对直观地访问结果:

using boost::fusion::at_key;

res = div(x, y);
int q = at_key<quotient>(res);
int r = at_key<remainder>(res);

还有其他优点,例如能够迭代地图的字段等。有关更多信息,请参阅doco

【讨论】:

  • 这是一些强大的元性。但它实际上在哪里胜过一个简单的结构呢?
  • But where does it actually win over a simple struct? -- 它可以减少一些构造函数的样板。
  • 你免费获得cout &lt;&lt;,你可以转换和组合/拆分结构
  • 这和 struct 一样快吗?
【解决方案3】:

对于元组,您可以使用tie,这有时非常有用:std::tr1::tie (quotient, remainder) = do_division ();。这对于结构来说并不容易。其次,在使用模板代码时,有时依赖对比为结构类型添加另一个 typedef 更容易。

如果类型不同,那么一对/元组确实不比结构差。例如pair&lt;int, bool&gt; readFromFile(),其中 int 是读取的字节数,bool 是 eof 是否被命中。在这种情况下添加一个结构对我来说似乎有点矫枉过正,特别是因为这里没有歧义。

【讨论】:

  • 即使类型不同,显式命名结构仍然可以提高可读性。
  • 但是你怎么知道它不是“平局”行中的余数/商?
  • 我认为我们在函数参数上也有同样的问题(虽然我们可以命名它们,它们的名称只是形式上的)。记录返回元组的文档可以说明什么位置对应于什么元素。
  • @litb。是的 - 这就是为什么我也更喜欢传递 in 结构而不是一长串参数。我也不喜欢 C++ 允许定义中的参数名称与声明中的参数名称不同。
  • 使用结构真的提高了可读性吗?这意味着我必须检查结构是如何定义的。我可能不在乎,我只需要从返回的对象中取出 int 和 bool。
【解决方案4】:

元组在 ML 或 Haskell 等语言中非常有用。

在 C++ 中,它们的语法使它们不太优雅,但在以下情况下很有用:

  • 你有一个必须返回多个参数的函数,但结果对于调用者和被调用者都是“本地的”;你不想仅仅为此定义一个结构

  • 您可以使用 tie 函数进行非常有限的模式匹配“a la ML”,这比使用结构来实现相同目的更优雅。

  • 它们带有预定义的

【讨论】:

    【解决方案5】:

    我倾向于将元组与 typedef 结合使用,以至少部分缓解“无名元组”问题。例如,如果我有一个网格结构,那么:

    //row is element 0 column is element 1
    typedef boost::tuple<int,int> grid_index;
    

    然后我使用命名类型为:

    grid_index find(const grid& g, int value);
    

    这是一个有点做作的例子,但我认为大多数时候它在可读性、明确性和易用性之间达到了一个令人满意的平衡。

    或者在你的例子中:

    //quotient is element 0 remainder is element 1
    typedef boost:tuple<int,int> div_result;
    div_result div(int dividend,int divisor);
    

    【讨论】:

      【解决方案6】:

      元组的一个特性是结构体所没有的,那就是它们的初始化。考虑以下内容:

      struct A
      {
        int a;
        int b;
      };
      

      除非您编写 make_tuple 等效项或构造函数,否则要将此结构用作输入参数,您首先必须创建一个临时对象:

      void foo (A const & a)
      {
        // ...
      }
      
      void bar ()
      {
         A dummy = { 1, 2 };
         foo (dummy);
      }
      

      不过,还不错,以维护出于任何原因向我们的结构添加新成员的情况为例:

      struct A
      {
        int a;
        int b;
        int c;
      };
      

      聚合初始化的规则实际上意味着我们的代码将继续编译而不改变。因此,我们必须搜索此结构的所有用法并更新它们,而无需编译器的任何帮助。

      将此与元组进行对比:

      typedef boost::tuple<int, int, int> Tuple;
      enum {
        A
        , B
        , C
      };
      
      void foo (Tuple const & p) {
      }
      
      void bar ()
      {
        foo (boost::make_tuple (1, 2));  // Compile error
      }
      

      编译器无法使用make_tuple 的结果初始化“Tuple”,因此会生成错误,允许您为第三个参数指定正确的值。

      最后,元组的另一个优点是它们允许您编写迭代每个值的代码。这根本不可能使用结构体。

      void incrementValues (boost::tuples::null_type) {}
      
      template <typename Tuple_>
      void incrementValues (Tuple_ & tuple) {
         // ...
         ++tuple.get_head ();
         incrementValues (tuple.get_tail ());
      }
      

      【讨论】:

      • 我认为元组中的项目越多,将其作为结构的理由就越好!关于聚合初始化,我 100% 同意你的观点,这就是为什么我几乎从不使用它的原因!带有构造函数(和 boost:optional)的结构对我来说效果很好。
      • Re:枚举元组成员 - 这是一个巧妙的技巧,但我认为它没有用处。
      【解决方案7】:

      防止你的代码被许多结构定义乱扔。当您只记录元组中的每个元素是什么时,编写代码的人以及使用它的其他人更容易,而不是编写自己的结构/让人们查找结构定义。

      【讨论】:

      • 在这种情况下,也许它们最好用在相对私有的接口中,例如用于来自私有成员函数的返回码。
      • 在哪里你会记录元组中每个元素的含义?在创建元组的方法中?在存储\使用元组的代码中?如果几个方法返回相同的元组布局怎么办?由于您没有得到正确的定义,因此也没有一个地方可以放置文档。
      【解决方案8】:

      元组将更容易编写 - 无需为每个返回内容的函数创建新结构。关于什么去哪里的文档将转到功能文档,无论如何都需要。要使用该函数,无论如何都需要阅读函数文档,其中将解释元组。

      【讨论】:

        【解决方案9】:

        我 100% 同意罗迪。

        要从一个方法返回多个值,除了元组之外,您还有几个选项,哪个最好取决于您的情况:

        1. 创建一个新结构。当您返回的多个值相关时,这很好,并且适合创建新的抽象。例如,我认为“divide_result”是一个很好的通用抽象,传递这个实体让你的代码比传递一个无名的元组更清晰。然后,您可以创建对该新类型进行操作的方法,将其转换为其他数字类型等。

        2. 使用“输出”参数。通过引用传递多个参数,并通过分配给每个 out 参数返回多个值。当您的方法返回多个不相关信息时,这是合适的。在这种情况下创建一个新结构将是矫枉过正,使用 Out 参数可以强调这一点,而且每个项目都会获得应有的名称。

        元组是邪恶的。

        【讨论】:

        • 如果正确使用元组,它们绝对不是邪恶的。 “Out”参数是邪恶的。
        • 如果元组是邪恶的,那么整个数据库行业就会陷入困境,因为数据库的整个概念都是从元组关系计算开始的。 en.wikipedia.org/wiki/Tuple_calculus
        • @douglasH 我很确定,鉴于这个问题的上下文以及随后的答案,“元组是邪恶的”语句指的是 std::tuple,C++ 编程语言中的一种数据结构...而不是元组关系演算中提到的那种元组。不过,常见的错误!
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-02-24
        • 1970-01-01
        • 1970-01-01
        • 2011-12-19
        • 2013-10-05
        • 2020-09-03
        • 2011-09-16
        相关资源
        最近更新 更多