6

我最近一直在网上寻找有关 C++ 概念的详细信息,并找到了几篇对几篇论文所称的“运行时概念”的引用。它们与编译时概念究竟有什么不同,为什么首先引入它们,它们将如何实现,为什么它们对 C++ 的未来很重要?通过浏览这些文件,我大致了解运行时概念旨在缓解当前存在于面向对象和通用代码之间的紧张关系,但我没有从中得到太多其他信息。

4

1 回答 1

7

这是我对正在发生的事情的理解。它从另一个角度开始:类型擦除。

std::function<void()>是类型擦除类的示例。它采用“无参数调用,不返回任何内容”的概念,以及“复制构造”和“销毁”的辅助概念,并将其包装成一个简洁的小包。

所以你可以做

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

并被groot调用。或者我们可以将 lambda、函数对象、std::bind表达式或 aboost::function传递给 astd::function并调用它。

所有这些类型都可以被复制、销毁和调用:因此std::function可以使用它们并生成单个运行时接口。除了它们支持的操作之外,std::function可以存储和执行的类型是无关的。没有将函数groot与 lambda 或boost::function.

它的构造函数std::function<void()>采用非 s 类型的东西,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!

这里我们有复制、销毁和调用 3 个概念,每一个都表示为一个纯虚拟函数。

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)} )
  {}
};

在这里,我们将我们想要支持的概念包装成所谓的Regular类型——值类型类型。我们有一个底层指针和虚拟层次结构(一个很小的,还没有看到),但是类型function看起来就像一个int——你可以复制、分配等等。

每个概念——调用、复制、移动、销毁——都被转发到pImpl(除了move,我们可以在这一层有效地实现)。

这里只完成了一半的类型擦除工作。这部分让我们将任何东西分配给我们的function类实例。我们可以通过测试通过概念要求来做得更好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() {}
};

func_type_eraser…我们实现为特定类型公开的概念接口T

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

我们可以更进一步:

我们甚至可以支持客户可以提供的任何功能来支持这些概念。

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

让我们的类型擦除实现不是直接与对象交互,而是调用 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))比较)。在每种情况下,它都可以使用“更专业”的实现。

所有这些都与概念在编译时的工作方式相似。除了,它发生在运行时,并且具有额外的类型擦除系统。

于 2014-10-03T20:35:04.020 回答