3个回答

49

摘要:

对于C++11,我会包括以下内容:

  • 移动构造函数(noexcept
  • 移动赋值(noexcept
  • 全序关系(使用 operator<() 实现自然全序关系,如果不存在自然全序关系,则使用 std::less<> )。
  • hash<>

并且会删除以下内容:

  • swap() (不抛出异常)- 由移动操作替代。

评论

Alex 在 Elements of Programming 中重新审视了正则类型的概念。实际上,该书的大部分内容都是关于正则类型的。

一组程序的包含,使我们能够将对象放置在数据结构中并使用算法将对象从一个数据结构复制到另一个数据结构,这样的类型被称为正则类型,因为它们的使用保证了行为的规律性和互操作性。 -- EoP第1.5节

EoP中,Alex引入了底层类型的概念,它给我们提供了一个非抛出的交换算法,可用于移动。在C++中,底层类型模板无法以任何特别有用的方式实现,但是您可以使用非抛出 (noexcept) 移动构造函数和移动赋值作为合理的近似值(底层类型允许将临时变量移动/复制到其他地方而不需要销毁)。在C++03中,如果提供了非抛出的swap(),则建议使用它来近似移动操作,如果提供了移动构造函数和移动赋值,则默认的std::swap()就足够了(尽管您仍然可以实现更有效的方法)。

[ 我已经公开发表过建议使用单一赋值运算符,通过值传递来涵盖移动赋值和复制赋值。不幸的是,当前语言规则中当类型获得默认移动构造函数时会导致复合类型出现问题。在语言中修复这个问题之前,您需要编写两个赋值运算符。然而,您仍然可以使用按值传递其他汇聚参数以避免处理所有参数的移动/复制的组合问题。 ]

Alex 还添加了完全排序的要求(尽管可能没有自然的完全排序,排序可能纯粹是表示性的)。operator<() 应该保留给自然的完全排序。如果自然的完全排序不可用,则建议专门化 std::less<>() (标准中也有先例)。

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

一个普通类型也应该是等式完备的(即,operator==() 应该可以作为非友元、非成员函数实现)。一个等式完备的类型也是可序列化的(虽然没有规范的序列化格式,但实现流运算符除了调试外几乎没什么用处)。一个等式完备的类型也可以被散列。在 C++11 中(或使用 TR1),您应该提供 std::hash 的特化。
另一个正则类型的属性是 area(),目前还没有任何标准语法 - 实际上除了测试之外可能很少有理由实现它。它是一个用于指定复杂度的有用概念 - 我经常为测试复杂度实现它(或其近似值)。例如,我们将复制的复杂度定义为对象区域复制的时间限制。
正则类型的概念并不是特定于语言的。当我面对一种新语言时,我首先要做的事情之一就是确定正则类型在该语言中的表现形式。

1
一个类型要成为正规的,即使在C++11中,该类型是否必须可复制?对于使用资源(如流或图像)的类型,能够实现这一点吗?我猜对于后一个问题的答案是否定的,但是对于前一个问题呢? - Germán Diago
2
我更新了链接。是的,即使在C++11中,为了被认为是规则的类型也必须是可复制的,甚至使用资源(图像、容器等)的类型也必须是可复制的。当复制的结果是无效或断开的关系时,我们可以选择不为表示关系的类型实现复制https://github.com/sean-parent/sean-parent.github.com/wiki/presentations/2014-04-14-goal-complete-types/goal-complete-types.pdf(视频尚未上线)。我更喜欢将这样的类型称为不完整的,而不是“不规则的”。 - Sean Parent
你建议除了可移动的类型,是否都要可以复制?我认为这可能更多是最好的努力而非总是可能的,我对吗? - Germán Diago
1
我建议将所有类型都设置为可复制 - 是的,这只是“尽力而为”。例外情况包括: - Sean Parent
4
(1) 代表可能在复制时被分离或失效的关系的对象 - 您可以选择不在此类对象上实现复制,但它们仅应用作构建其他常规类型的机制。 (2) 不完整的等式对象 - 通常可以通过使对象不可变来实现复制。 (3) 来自库(平台、操作系统)的不可复制类型 - 可复制是一个传递性质,因此包含这些类型的对象不可复制。使用不可变性或等式完备性来恢复复制。 - Sean Parent
显示剩余2条评论

11

泛型编程的限制最好用表达式来说明。对于复制性的相同限制的更现代版本是,以下两个语句都应该是有效的:

T b = a;

并且

T b = ra;

其中a是具有类型Tconst T的lvalue,ra是具有类型Tconst T的rvalue。(后置条件类似。)

我认为这个表述符合论文的精神。请注意,C++03已经使用了像lvalues和rvalues这样的概念,这意味着我们所表达的约束需要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是否具有构造函数或赋值运算符。

我建议您通过在表达式级别上表示约束来真正进行通用编程,因为例如,移动构造函数的存在或不存在既不足够也不必要使一种类型可以被复制构造。


1
添加移动赋值运算符和移动复制构造函数,以及内置类型的所有其他运算符,根据Stepanov的论文,我认为你就做到了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接