C++运行时概念是什么?

6

最近我在网上查找关于C++概念的细节,发现了几篇论文称为“运行时概念”。它们与编译时概念有什么不同,首先为什么引入它们,它们将如何实现,以及为什么它们对C++的未来至关重要?从浏览这些论文,我大致了解到运行时概念旨在缓解目前面向对象和泛型代码之间存在的紧张关系,但其他内容并不多。


如果他们不能解释得让人理解,那么这就不重要了;-) - Cheers and hth. - Alf
1
对我来说,他们听起来像是非常喜欢编写ObjectiveC程序,但从未接触过该语言的人。相反,他们试图在不质疑有关该语言的普遍信念的情况下,在C++中增加一些灵活性。仅仅因为这是他们所知道的语言。如果他们既了解ObjectiveC又了解C ++,那么他们将以与C ++程序通常看到的风格截然不同的方式使用C ++,而不是尝试通过修复语言来修复该风格的问题。因此,他们只会为该语言的功能膨胀做出贡献... - cmaster - reinstate monica
@cmaster:让他们去折腾吧;一旦Swift流行起来,Objective-C最终会被淘汰的。我认为这是一个相当不错的想法。现在如果他们能更好地解释一下就好了...哼。 - RandomDSdevel
1
你了解类型擦除吗?这似乎是很多人关注的问题。然后,有一些关于穿透类型擦除屏障以允许算法访问底层数据的内容(基本上是类型擦除算法执行),同时允许客户端类型提供这样的改进。还有一个概念框架来包装所有这些内容,类似于即将到来的C++编译时概念模型。请注意,类型擦除不仅仅是std::function,但它确实是其中的起点。或者我可能反过来了:类型擦除是如何将具体类型与要擦除的概念结合起来,制作运行时类型擦除实例的。 - Yakk - Adam Nevraumont
嗯...好吧,我觉得你刚刚让我迷惑了。 - RandomDSdevel
显示剩余4条评论
1个回答

8

这是我对事情的理解。它从不同的角度开始:类型擦除。

std::function<void()> 是一个类型擦除类的例子。它将“无参数调用且不返回任何内容”的概念与“复制构造”和“销毁”的辅助概念结合起来,打包成一个整洁的小包裹。

所以你可以这样做:

void groot () { std::cout << "I am groot!\n"; }
std::function<void()> f = groot;
f();

并且调用groot。或者我们可以传递lambda、函数对象、std::bind表达式或boost::functionstd::function并调用它们。

所有这些类型都可以被复制、销毁和调用:因此std::function可以消耗它们并产生一个单一的运行时接口。除了支持的操作之外,std::function可以存储和执行的类型没有关联。没有一个类层次结构将函数groot与lambda或boost::function联系起来。

std::function<void()>的构造函数,它接受不是std::function的东西,在复制、销毁和使用签名为void()的概念下对其进行类型抹除。

我们从这里开始:

template<class Sig>
struct func_type_eraser;

template<class R, class... Args>
struct func_type_eraser<R(Args...)> {
  // invoke:
  virtual R operator()(Args...) const = 0;
  // copy:
  virtual func_type_eraser* clone() const = 0;
  // destroy:
  virtual ~func_type_eraser() {};
};
template<class Sig, class T>
struct func_type_eraser_impl; // TODO!

这里有三个概念:copy(复制)、destroy(销毁)和invoke(调用),每个概念都表示为一个纯虚函数。

template<class Sig>
struct function;
template<class R, class... Args>
struct function<R(Args...)> {
  std::unique_ptr<func_type_eraser<R(Args...)>> pImpl;
  // invoke:
  R operator()( Args... args ) const {
    return (*pImpl)( std::forward<Args>(args)... );
  }
  // destroy:
  ~function() = default;
  // copy:
  function(function const& o) : pImpl( o.pImpl ? o.pImpl->clone() : nullptr ) {}
  // move:
  function(function&&) = default;
  // TODO: operator=

  // technical issues, ignore:
  function(function& o) : function(const_cast<function const&>(o)) {}
  function(function const&& o) : function(o) {}

  // type erase:
  template<class T>
  function(T&& t) : pImpl( new func_type_eraser_impl<R(Args...), std::decay_t<T>>{std::forward<T>(t)} )
  {}
};

在这里,我们将要支持的概念封装成一个称为“常规”类型的值类型类型。我们有一个基础指针和虚拟层次结构(一个小型的、尚未见过的层次结构),但是类型“function”看起来就像一个“int”——你可以复制、赋值等操作。
每个概念——调用、复制、移动、销毁——都被转发到pImpl(除了move,在这一层中我们可以有效地实现它)。
这里只完成了类型抹除工作的一半。这部分让我们可以将任何东西赋给我们的function类实例。如果在接受T到我们的构造函数之前测试T是否通过了概念需求——即它可以被复制、销毁,并使用所需的签名进行调用——我们可以做得更好。(当前的C++ std::function未能做到这一点,令人非常烦恼)。
类型抹除的最后一部分是...:
template<class R, class... Args, class T>
struct func_type_eraser_impl<R(Args...), T> : func_type_eraser<R(Args...)> {
  // type erase storage:
  T t;
  // invoke:
  virtual R operator()(Args... args) const override {
    return t( std::forward<Args>(args)... );
  }
  // copy:
  virtual func_type_eraser_impl* clone() const override {
    return new func_type_eraser_impl{t};
  }
  // destroy:
  virtual ~func_type_eraser_impl() {}
};

在这里,我们为特定类型T实现了func_type_eraser中公开的概念接口。

现在我们有4个概念,其中3个是类型擦除的,另一个由我们常规的类型包装处理,我们可以存储支持这3个概念的任何内容。

我们可以更进一步:

我们甚至可以支持客户端提供函数来支持这些概念的任何内容。

最简单的方法是在允许参数相关查找(ADL)的上下文中调用一个自由函数,例如std::begin

我们的类型擦除实现不直接与对象交互,而是在ADL上下文中调用自由函数。

提供该函数的默认实现,该实现从“失败”到“检查是否存在.begin()方法并调用它”或“执行低效版本”或“检查传递的类型的属性,并确定完成任务的合理方式”,等等。

使用此技术,我们可以允许客户端扩展我们的类型擦除,并使用更广泛的概念。

作为一个具体的例子,想象一下我们有可打印的概念。如果已经重载了ostream << X,或者如果已经重载了print(X),则可以打印某些内容。

我们向类型擦除接口添加print_it。它using impl_namespace::print,然后执行print(t)

impl_namespace::print(X)只是执行cout << X

这是完全解耦的。您可以使用其他人编写的没有打印概念的类型,在其命名空间中添加自由函数以添加打印概念,然后将其传递给我们的类型擦除系统,类型擦除系统会将其连接起来。

请参见此频道9视频,其中有人使用类似的技术构建了一款带有无限撤消和显示的玩具文档,可以扩展到任意数量的类型,包括内置类型。

现在,想象一下对此的语言支持。能够描述一组要进行类型擦除的概念,并说“构建一个常规类型以擦除这些类型”。

如果您有一个算法受其他概念支持,则可以说“类型擦除支持此算法”。了解该算法类型擦除的任何客户端都可以自动将其添加到您的接口中。那些不知道的人可以使用您提供的类型擦除概念来实现它。

在类型抹除的时候,您的概念从编译时期的理解被转化为虚拟和运行时期,您算法的类型抹除支持可以非常高效,即使对于您类型上的概念支持是基于概念映射的(即,提供了自定义函数来解决问题)。您的类型并不是简单可复制的,但是有一个克隆函数,将其复制到适当的存储空间中。算法概念类型抹除可以考虑到完整的编译时概念映射而不是运行时虚拟概念映射,即使没有根本上更快的算法,也能获得性能提升。
如果非常小心地做,您可以将具有较少概念的类型抹除对象扩展为具有更多概念的对象,如果新概念由较少的概念支持,则客户端将从他们的运行时接口中支持它:那些知道会为您的类型提供定制的快速二进制搜索。
再迈出一步,您可以在类型抹除类中拥有可选概念支持。例如,类型抹除的迭代器可以选择支持随机访问迭代。接受迭代器的算法可能会测试随机访问迭代,并在有支持的情况下创建更好的实现。对于二分搜索范围的概念,可能会检查范围是否具有二分搜索概念支持,如果没有,则检查是否具有随机访问支持,如果还不行,使用前向迭代器版本的二进制搜索(O(n) 次推进,O(lg(n)) 次比较)。在每种情况下,它都可以使用“更专业化”的实现。
所有这些都与编译时概念的工作方式相似。除了发生在运行时并具有额外的类型抹除系统。

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