【问题标题】:What is a "Regular Type" in the context of move semantics?什么是移动语义上下文中的“常规类型”?
【发布时间】:2012-12-22 00:32:30
【问题描述】:

Alex Stepanov defined Regular Types 作为满足复制和相等某些属性的类型。现在 C++11 已将移动语义添加到泛型编程领域,Stepanov 的定义不再完整。我正在寻找关于常规类型的良好参考,包括它们与移动语义的交互。

【问题讨论】:

    标签: c++ generics c++11 move-semantics


    【解决方案1】:

    总结:

    对于 C++11,我将包括:

    • move-ctor (noexcept)
    • 移动分配 (noexcept)
    • 总排序(operator<() 表示自然总排序,std::less<> 表示自然排序 总订单不存在)。
    • hash<>

    并且会删除:

    • swap()(非投掷)- 替换为移动操作。

    评论

    Alex 在Elements of Programming 中重新审视了常规类型的概念。事实上,本书的大部分内容都是关于常规类型的。

    有一组程序包含在计算 类型的基础让我们将对象放置在数据结构中并使用 将对象从一种数据结构复制到另一种数据结构的算法。我们称之为 具有这种基础的类型经常,因为它们的使用保证 行为的规律性,因此,互操作性。 -- EoP

    的第 1.5 节

    EoP 中,Alex 引入了基础类型的概念,它为我们提供了一种可用于移动的非抛出交换算法。底层类型模板在 C++ 中不能以任何特别有用的方式实现,但您可以使用非抛出 (noexcept) move-ctor 和 move-assign 作为合理的近似值(底层类型允许在没有临时的额外破坏)。在 C++03 中,提供非抛出 swap() 是近似移动操作的推荐方法,如果您提供 move-ctor 和 move-assign 那么默认 std::swap() 就足够了(尽管您仍然可以实现更多高效的)。

    [ 我on record 建议您使用单个赋值运算符,按值传递,以涵盖移动分配和复制分配。不幸的是,当一个类型获得一个默认的 move-ctor 时,当前的语言规则会导致它与复合类型中断。在语言中修复之前,您将需要编写两个赋值运算符。但是,您仍然可以对其他接收器参数使用按值传递,以避免在处理所有参数的移动/复制时使用组合。 ]

    Alex 还增加了总排序的要求(尽管可能没有自然的总排序,并且排序可能纯粹是代表性的)。 operator<() 应保留用于自然总排序。我的建议是专门化std::less<>(),如果自然总排序不可用,标准中有一些先例。

    EoP 中,Alex 放宽了对平等的要求,以允许表示平等就足够了。一个有用的改进。

    常规类型也应该是等式完整的(也就是说,operator==() 应该可以作为非朋友、非成员函数实现)。等式完整的类型也是可序列化的(尽管没有规范的序列化格式,实现流运算符除了调试之外几乎没有用处)。等式完备的类型也可以被散列。在 C++11(或 TR1)中,您应该提供 std::hash 的特化。

    常规类型的另一个属性是area(),它还没有任何标准语法——除了测试之外,可能没有什么实际实现的理由。对于指定复杂性,这是一个有用的概念——我经常实现它(或近似值)来测试复杂性。例如,我们将复制的复杂度定义为以复制对象区域的时间为界。

    常规类型的概念不是特定于语言的。当遇到一门新语言时,我做的第一件事就是弄清楚常规类型在该语言中的表现方式。

    【讨论】:

    • 对于常规类型,该类型是否必须是可复制的,即使在 C++11 中也是如此?这可以通过使用资源的类型(例如流或图像)来实现吗?我猜后一个问题的答案是否定的,但前一个问题呢?
    • 我更新了链接。是的,即使在 C++11 中,类型也必须是可复制的才能被视为常规类型,甚至使用资源(图像、容器等)的类型也必须是可复制的。当复制的结果是无效的或被切断的关系 github.com/sean-parent/sean-parent.github.com/wiki/…>(视频尚未在线)时,我们可能会选择不为表示关系的类型实施复制。我更喜欢将此类类型称为不完整的,而不是“不规则的”。
    • 除了可移动之外,您是否建议使所有类型都可复制?我认为这将比以往任何时候都更加努力,对吗?
    • 我确实建议让所有类型都可复制 - 是的,这是“尽力而为”。例外情况是:
    • (1) 表示可能在复制时被切断或失效的关系的对象 - 您可以选择不在此类对象上实现复制,但它们应仅用作构建其他常规类型的机制。 (2) 等式不完整的对象——通常可以通过使对象不可变来实现复制。 (3) 库(平台、操作系统)中不可复制的类型 - 可复制是传递属性,因此包含此类类型的对象不可复制。使用不变性或等式完整性来恢复副本。
    【解决方案2】:

    泛型编程的约束最好用表达式来表述。对可复制性的相同约束的更现代的演绎是两个陈述都应该是有效的:

    T b = a;
    

    T b = ra;
    

    其中aTconst T 类型的左值,raTconst T 类型的右值。 (具有类似的后置条件。)

    我相信,这个提法符合论文的精神。请注意,C++03 已经使用了左值和右值之类的概念,因此我们所表达的约束要求 T source(); T b = source(); 之类的东西是有效的——当然这似乎是合理的。

    在这些限制下,C++11 并没有太大的变化。特别值得注意的是,这种(病态)类型是不规则的:

    struct irregular {
        irregular() = default;
        irregular(irregular const&) = default;
        irregular& operator=(irregular const&) = default;
    
        irregular(irregular&&) = delete;
        irregular& operator=(irregular&&) = delete;
    };
    

    因为像irregular a; irregular b = a; 这样的东西是有效的,而irregular source(); irregular b = source(); 不是。这是一种有点可复制的类型(分别是可复制的),但还不够。 [这被认为是一个缺陷,并计划在 C++1y 中进行更改,这种类型实际上是可复制的。 ]

    更进一步,对于副本必须在某种意义上与原始副本(或者,对于右值,副本之前的原始副本)等价的后置条件,移动特殊成员只能是“优化” ' 相应的复制特殊成员。另一种说法是复制语义是移动语义的改进。这意味着断言必须满足以下条件:

    T a;
    T b = a;
    T c = std::move(a);
    assert( b == c );
    

    即无论我们是通过复制“请求”(即涉及左值源的表达式)还是通过移动请求(涉及右值源的表达式)到达那里,无论“实际”发生了什么(无论是涉及复制特殊成员或移动特殊成员(如果有)。

    有趣的是,像 std::is_copy_constructible 这样的特征过去被称为 std::has_copy_constructor,但被重命名为强调表达式而不是内在属性:像 std::is_copy_constructible<int>::value && std::is_move_assignable<int>::value 这样的东西是真的,不管事实是int 没有构造函数或赋值运算符。

    我建议您通过在表达式级别上表达约束来真正进行泛型编程,因为例如移动构造函数的存在与否对于可复制构造的类型来说既不充分也没有必要。

    【讨论】:

      【解决方案3】:

      根据 Stepanov 的论文,添加移动赋值和移动复制构造函数,以及所有其他内置类型的运算符,我会说你拥有它。

      【讨论】:

        猜你喜欢
        • 2021-08-29
        相关资源
        最近更新 更多