最近我在网上查找关于C++概念的细节,发现了几篇论文称为“运行时概念”。它们与编译时概念有什么不同,首先为什么引入它们,它们将如何实现,以及为什么它们对C++的未来至关重要?从浏览这些论文,我大致了解到运行时概念旨在缓解目前面向对象和泛型代码之间存在的紧张关系,但其他内容并不多。
最近我在网上查找关于C++概念的细节,发现了几篇论文称为“运行时概念”。它们与编译时概念有什么不同,首先为什么引入它们,它们将如何实现,以及为什么它们对C++的未来至关重要?从浏览这些论文,我大致了解到运行时概念旨在缓解目前面向对象和泛型代码之间存在的紧张关系,但其他内容并不多。
这是我对事情的理解。它从不同的角度开始:类型擦除。
std::function<void()>
是一个类型擦除类的例子。它将“无参数调用且不返回任何内容”的概念与“复制构造”和“销毁”的辅助概念结合起来,打包成一个整洁的小包裹。
所以你可以这样做:
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!
这里有三个概念: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)} )
{}
};
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视频,其中有人使用类似的技术构建了一款带有无限撤消和显示的玩具文档,可以扩展到任意数量的类型,包括内置类型。
现在,想象一下对此的语言支持。能够描述一组要进行类型擦除的概念,并说“构建一个常规类型以擦除这些类型”。
如果您有一个算法受其他概念支持,则可以说“类型擦除支持此算法”。了解该算法类型擦除的任何客户端都可以自动将其添加到您的接口中。那些不知道的人可以使用您提供的类型擦除概念来实现它。
在类型抹除的时候,您的概念从编译时期的理解被转化为虚拟和运行时期,您算法的类型抹除支持可以非常高效,即使对于您类型上的概念支持是基于概念映射的(即,提供了自定义函数来解决问题)。您的类型并不是简单可复制的,但是有一个克隆函数,将其复制到适当的存储空间中。算法概念类型抹除可以考虑到完整的编译时概念映射而不是运行时虚拟概念映射,即使没有根本上更快的算法,也能获得性能提升。
std::function
,但它确实是其中的起点。或者我可能反过来了:类型擦除是如何将具体类型与要擦除的概念结合起来,制作运行时类型擦除实例的。 - Yakk - Adam Nevraumont