我最近一直在网上浏览有关C ++概念的详细信息,并且已经找到了几篇论文称为“run-time concepts”的文章。它们与编译时概念有何不同,为什么它们首先被引入,它们将如何实现,以及它们为什么对C ++的未来很重要?通过浏览论文,我得到了一个大概的观点,即运行时概念旨在缓解当前存在于面向对象和通用代码之间的紧张局面,但我没有从中得到更多其他东西。
答案 0 :(得分:7)
这是我对正在发生的事情的理解。它从不同的角度开始:类型擦除。
std::function<void()>
是类型擦除类的一个示例。它采用了没有参数的&#34;调用的概念,并且没有返回&#34;,以及&#34;复制构造&#34;的辅助概念。并且&#34;销毁&#34;,并将它包装成一个整洁的小包装。
所以你可以做到
void groot () { std::cout << "I am groot!\n"; }
std::function<void()> f = groot;
f();
和groot
被调用。或者我们可以将lambda,或函数对象,std::bind
表达式或boost::function
传递给std::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!
这里我们有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个概念的任何内容。
我们可以更进一步:
我们甚至可以支持客户端可以提供支持这些概念的任何功能。
最简单的方法是在允许ADL(依赖于参数的查找)的上下文中调用自由函数,如std::begin
。
让我们的类型擦除实现不是直接与对象交互,而是调用ADL上下文中的自由函数。
提供该函数的默认实现,该函数执行“&#34;失败”#34; to&#34;检查方法.begin()
并调用它&#34;或&#34;做一个效率低下的版本&#34;或者&#34;检查传递类型的属性,并确定执行任务的合理方式&#34;。
通过这种技术,我们可以允许客户端扩展我们的类型擦除,并使用更广泛的概念。
作为一个具体的例子,假设我们有可打印的概念。如果某项内容已ostream << X
重载,或者print(X)
已超载,则可以打印。
我们将print_it
添加到我们的类型擦除界面。它using impl_namespace::print
,然后执行print(t)
。
impl_namespace::print(X)
只需cout << X
。
这一切都是分离的。您可以采用其他人编写的类型,没有打印概念,通过其命名空间中的自由函数添加打印概念,然后将其传递给我们的类型擦除系统,类型擦除系统将其连接起来。
有关使用类似技术构建具有无限撤消和显示的玩具文档的人的示例,请参阅this channel 9 video,该文档可以扩展为任意数量的类型,包括内置类型。
现在,想象一下语言支持。能够描述一组你想要输入的概念,并说出&#34;建立一个删除这些类型的常规类型&#34;。
如果您拥有所述其他概念所支持的算法,则可以说&#34;键入此算法的擦除支持&#34;。任何了解算法类型擦除的客户端都可以自动为您的界面添加自定义创建的客户端。那些不能使用您提供的类型擦除概念来实现它。
在类型擦除时,您的概念在编译时被理解为虚拟和运行时,对您的算法的类型擦除支持可以非常高效,即使对您的类型的概念的支持是基于概念图(即,提供解决问题的自定义函数。您的类型不是天真可复制的,但是有一个克隆函数可以将其复制到合适的存储中,比如说)。算法概念类型擦除可以考虑完整的编译时概念映射而不是运行时虚拟概念映射,即使没有基本上更快的算法也能提高性能。
如果非常小心,您可以使用较少概念的类型擦除对象,并将概念扩展为具有更多概念的类型,如果新概念受较少概念支持。 “#34;不知道&#34;你想要一个快速的二进制搜索(比如说)最终会从它们的运行时界面支持它:那些能够为你提供为你的类型定制的快速二进制搜索。
再迈一步,您可以在类型擦除类中使用可选的概念支持。作为示例,类型擦除迭代器可以可选地支持随机访问迭代。接受迭代器的算法可能会测试随机访问迭代,如果是,则创建更好的实现。对范围进行二进制搜索的概念可能会检查范围是否具有二进制搜索概念支持,如果没有,则检查是否具有随机访问支持,并且如果没有使用,则使用二进制搜索的迭代器版本(O(n)advance,O(lg( n))比较)。在每种情况下,它都可以使用&#34;更专业的&#34;实施
所有这些都与概念在编译时的工作方式相似。除此之外,它发生在运行时,并具有额外的类型擦除系统。