恒定时间"缺点"

Constant time 'cons'

本文关键字:缺点 定时间      更新时间:2023-10-16

有没有办法(例如,修改参数类型(使以下"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

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

您似乎正在用一个更糟糕的std::make_tuple助手重新发明std::tuple。请改用它。该标准没有为 std::tuple 的转发构造函数或std::make_tuple提供复杂性保证,但它有点没有意义,因为这两个使用完美的转发,因此每个元素只有一个移动/复制/放置构造用于调用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的调用中有三个临时int,然后从对make_tuple内部std::forward<int>(t)...的第一次调用中有三个int&& x值,它们绑定到构造函数的参数,这些参数再次作为int&& x值转发给从它们构造的std::tuple<int, int, int>的概念上的三个成员。


刚刚意识到我所说的构造数量仅适用于对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<int&&, int&&, int&>。此帮助程序从不返回带有值的元组,只返回引用的"浅"元组。然后,std::tuple<int, int, int> 的相应转换构造函数将通过两次移动和一个副本初始化其"成员"。请注意,这种愚蠢的(非(优化使您面临编写auto tuple = std::forward_as_tuple(1, 2, i);的风险,这是一个具有两个悬空引用的元组。

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

(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结构可能会被优化掉。