【问题标题】:Constant time 'cons'恒定时间“缺点”
【发布时间】:2011-08-08 01:07:14
【问题描述】:

有没有办法(例如,修改参数类型)使以下“cons”函数花费恒定时间而不是 O(n) 时间。即构建列表应该花费 O(n) 时间,而不是 O(n^2) 时间。

我可以在以下情况下这样做吗:

(1) 无动态内存分配
(2) x 必须仍然有效(即可能没有对临时对象的引用)
(3) HEAD 类型可能有一个单独编译的构造函数(不可内联)

#include <type_traits>

template <typename HEAD, typename TAIL>
struct List
{
  List(HEAD head, TAIL tail) : head(head), tail(tail) {}
  typename std::remove_reference<HEAD>::type head;
  typename std::remove_reference<TAIL>::type tail;
};

template <typename HEAD, typename TAIL>
List<HEAD, TAIL> cons(HEAD head, TAIL tail)
{
  return List<HEAD, TAIL>(head, tail);
}

struct Empty {};

int main()
{
  auto x = cons(1, cons(2, cons(3, Empty())));
  // x should still be valid here
}

如何工作的示例。

编译器知道 x 的类型,因此在堆栈上分配空间。

所以堆栈看起来像这样:

| Int1 | Int2 | Int3 |
| p    | p+4  | p+8  |

其中p 是任意地址。

编译器创建调用cons(2, cons(3, Empty())),将返回值指向p+4

cons(2, cons(3, Empty())) 内部,编译器创建对cons(3, Empty()) 的调用,将返回值指向p+8

这样,每次调用cons,就不需要复制tail了。

我只是不确定代码,因此编译器可以(如,被允许)进行此优化。不过,如果有另一种获得恒定运行时间的方法,我很乐意使用它。

【问题讨论】:

  • 我不明白您的“无动态内存分配”要求。这不是cons 通常所做的吗?
  • 你能解释一下你现在的cons是线性的吗?有固定数量的参数不以任何方式检查。你是说最后的表达吗?
  • 所以你正在制作一个链接列表。它包含的不是对事物的引用,而是它们的副本。所以我猜 O(n^2) 来自这样一个事实,即每个嵌套的 cons 调用都会复制其参数,它将每个先前创建的对象复制到新的对象中。

标签: c++ algorithm c++11


【解决方案1】:

您似乎正在用更糟糕的 std::make_tuple 助手重塑 std::tuple。改用那个。该标准没有为 std::tuplestd::make_tuple 的转发构造函数提供复杂性保证,但它有点没有实际意义,因为这两个使用完美转发,所以每个元素只有一个 move/copy/emplace 构造用于调用std::make_tuple;剩下的就是改组引用。至少是线性数量的构造。

当然,不能保证您的编译器会优雅地处理所有引用改组,但无论如何您都在错误的级别进行优化。


为了说明的目的,几乎但不完全是发生了什么:

template<typename... T>
class tuple {
    T... members; // this is not correct, only here for illustration

    // The forwarding constructor
    template<typename... U>
    explicit
    tuple(U&&... u)
        : member(std::forward<U>(u)...)
    {}
};

template<typename... T>
tuple<typename std::decay<T>::type...>
make_tuple(T&&... t)
{ return tuple<typename std::decay<T>::type...>(std::forward<T>(t)...); }

因此,在对auto tuple = std::make_tuple(1, 2, 3) 的调用中,对make_tuple 的调用有三个临时的ints,然后是在make_tuple 内部对std::forward&lt;int&gt;(t)... 的第一次调用的三个int&amp;&amp; xvalues,它们绑定到构造函数的参数再次作为int&amp;&amp; xvalues 转发给std::tuple&lt;int, int, int&gt; 的概念三个成员,这些成员是从它们构造的。


刚刚意识到我所说的构造数仅适用于对std::make_tuple 的调用,而不是整个表达式auto tuple = std::make_tuple(...);。由于元组是从函数返回的,因此可能需要将移动/RVO 移动到最终变量。它仍然是线性的,它仍然是编译器喜欢优化的东西之一,它仍然是担心优化的错误点。

但是,C++0x 充满了优点,它已经可以做到你在回答中描述的那样:

int i = 3;
std::tuple<int, int, int> tuple = std::forward_as_tuple(1, 2, i);

forward_as_tuple 的调用将返回std::tuple&lt;int&amp;&amp;, int&amp;&amp;, int&amp;&gt;。这个助手从不返回一个带有值的元组,只返回一个“浅”的引用元组。然后,std::tuple&lt;int, int, int&gt; 的适当转换构造函数将使用两个移动和一个副本初始化其“成员”。请注意,这种愚蠢的(未)优化会让您面临编写 auto tuple = std::forward_as_tuple(1, 2, i); 的风险,这是一个带有两个悬空引用的元组。

【讨论】:

    【解决方案2】:

    我将在调查后回答我自己的问题,但如果有人知道更好的方法,我仍然会感兴趣。

    (1) 将List 重命名为TempList
    (2)将TempList的构造函数(包括移动和复制)设为私有。
    (3) 让cons成为TempList的朋友。
    (4) 将HEADTAIL 成员设为引用。

    现在TempList 只保存引用,因此没有副本。然而,这些可能是对临时对象的引用,但这没关系,因为临时对象持续到表达式的末尾,并且因为 TempList 只有私有构造函数,它不能分配给表达式的 LHS。只有cons 可以创建TempList,并且不能超过创建它的表达式。

    现在创建一个函数save 或类似效果的函数,它接受TempList 并返回一个真正的List,这是另一种按值存储数据的类型。

    所以我们有

    auto x = save(cons(1, cons(2, cons(3, Empty()))));

    并且数据最多会被复制或移动一次(通过保存),整个结构现在是O(n)。只要conssave 都被内联,TempList 结构可能会被优化掉。

    【讨论】:

    • 这并不能解释这比std::tuple&lt;int, int, int&gt;(1, 2, 3) 更好。
    • @Nicol Bolas:抱歉,这可能不是一个很好的例子,但让我们说 cons 我们有 f1f2f3。说joinremoveinsert 等。你如何为 std::tuple 编写这样的函数而不在每一步都执行复制?
    • 无论如何,您都必须使用您在此处执行的任何内部操作来执行复制或移动,因为您的 List 包含实际对象本身,而不是对它们的引用。如果没有复制/移动或指针,您将无法实现 joinremoveinsert。如果可以的话,它已经是std::tuple 的一部分了。
    • @Nicol Bolas: TempList 只有参考文献。最后只有一个副本,当我们形成List 时,不是中间副本。
    • 是的,但是一旦你有了List,那就结束了。您所做的只是对表达式的惰性求值。一旦你评估它,你就有一个只能复制/移动或引用的冻结对象。
    猜你喜欢
    • 1970-01-01
    • 2010-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多