运行时与编译时多态性:更好的可读性与编译时错误检查,哪个更重要?
Runtime vs compile-time polymorphism: better readability vs compile-time errors checks, what is more important?
现在很多关于 C++ 的演讲都是关于模板及其在编译时多态性实现中的用法;几乎不讨论虚函数和运行-时间多态性。
我们可以在很多情况下使用编译时多态性。因为它给了我们编译时检查而不是可能的 运行 时间多态性相关的 运行 时间错误,以及一些(通常微不足道的)性能优势,它看起来是当今使用最广泛的库比 运行-time 更喜欢编译时多态性。
但是,对我来说,与虚拟类型层次结构相比,使用 C++ 模板实现的编译时多态性导致的自我记录和可读代码要少得多。
我们可以回顾一下现实生活中的例子boost::iostreams
。它实现 stream
作为接受设备 class 作为参数的模板。这会导致特定功能的实现被分成许多 classes 和不同文件夹中的文件的情况,因此对此类代码的调查比流将形成 classes 具有虚拟功能的层次结构要复杂得多,例如我们有 Java 和 .NET Framework?编译时多态性在这里有什么好处?文件流是读取和写入文件的东西,流是读取和写入(任何东西)的东西,它是类型层次结构的 class 典型示例,所以为什么不使用单个 FileStream
class重载一些受保护的功能,而不是将语义统一的功能划分到不同的文件和 classes?
另一个例子是boost::process::child
class。它使用模板化构造函数来设置标准 i/o 和其他过程参数。它没有很好的文档记录,并且从这个函数原型中看不出这个模板将接受什么格式的参数;类似于 SetStandardOutput
的成员函数的实现将更好地自我记录并导致更快的编译时间,那么在这里使用模板有什么好处呢?同样,我将此实现与此处的 .NET Framework 进行比较。对于类似于 SetStandardOutput
的成员函数,阅读单个头文件就足以了解如何使用 class。对于 boost::process::child
的模板构造函数,我们必须读取许多小文件。
有很多类似的例子。出于任何原因,众所周知的开源库几乎从不使用虚拟 classes 层次结构,而是更喜欢像 boost
那样使用编译时多态性(主要是基于模板的)。
问题:在我们可以同时使用两者的情况下,我们必须选择什么(编译时多态性或 运行 时多态性)有明确的指导方针吗?
Generally speaking, in 90% of situation templates and virtual functions are interchangeable.
首先,我们需要澄清一下我们在说什么。如果你 "compare" 某些东西,它必须在某些标准中是等价的。我对您的陈述的理解不是将 virtual functions
与 templates
进行比较,而是在 polymorphism
!
的上下文中进行比较
在那种情况下你的例子没有很好地选择,而且 dynamic cast
更像是一个 "big hammer" 工具箱之外的东西,就好像我们谈论多态性一样。
你的 "template example" 不需要使用模板,因为你有简单的重载,根本不需要任何模板代码就可以使用!
如果我们谈论 polymorphism
和 c++
,我们首先选择 runtime-polymorphism
和 compile-time
多态性。对于这两者,我们都有标准的 C++ 解决方案。对于运行时,我们使用虚函数,对于编译时多态性,我们将 CRTP 作为典型实现,而不是模板作为通用术语!
are there any comments or recommendations from C++ committee or any other authoritative source, when we have to prefer ugly syntax of templates over much more understandable and compact virtual functions and inheritance syntax?
用惯了语法不难看!如果我们谈论用 SFINAE 实现东西,我们有一些难以理解的模板实例化规则,尤其是经常被误解的 deduced context
.
但在 C++20 中我们将有 concepts,它可以在大多数情况下取代 SFINAE,我认为这是一件好事。使用概念而不是 SFINAE 编写代码使其更具可读性、更易于维护并且更容易扩展新类型和 "rules".
Standard library if full of templates and has a very limited amount of virtual functions. Does it mean that we have to avoid virtual functions as much as possible and prefer templates always even if theirs syntax for some specific task is much less compact and much less understandable?
题目感觉你误解了C++的特性。模板允许使用 generic code
而 virtual functions
是实现 runtime polymorphism
的 C++ 工具。没有什么是 1:1 可比的。
在阅读您的示例代码时,我建议您重新考虑一下您的编码风格!
如果您想编写针对不同数据类型的函数,只需像在 "template" 示例中那样使用重载,但不要使用不需要的模板!
如果您想在同一代码中实现适用于不同数据类型的通用函数,请使用模板,如果特定数据类型需要一些特殊代码,请对选定的特定代码部分使用模板专门化.
如果您需要更多需要 SFINAE 的选择性模板代码,您应该开始使用 c++20 概念来实现。
如果要实现polymorphism
决定使用run time
或compile time
多态性。如前所述,对于第一个,虚函数是实现它的标准 C++ 工具,而 CRTP 是第二个的标准解决方案之一。
而我对 "dynamic cast" 的个人体验是:避免它!这通常是您的设计出现问题的第一个提示。这不是一般规则,而是重新考虑设计的检查点。在极少数情况下,它是适合的工具。在 RTTI 中,并非对所有目标都可用,并且它有一些开销。在裸机 devices/embedded 系统上,您有时无法使用 RTTI 和异常。如果您的代码打算在您的域中用作 "platform",并且您有上述限制,请不要使用 RTTI!
编辑:来自评论的回答
So, for now, with C++ we can make classes hierarchy with run-time polymorphism only.
没有! CRTP 还构建 class 层次结构,但用于编译时多态性。但是解决方案完全不同,因为您没有 "common" 基础 class。但由于所有内容都在编译时解决,因此对公共基础 class 没有技术需求。您应该开始阅读有关 Mixins 的内容,也许在这里:What are Mixins (as a concept) and CRTP as one of the implementation methods: CRTP article wikipedia.
don't know how to implemented something similar to virtual functions without run-time overhead.
参见上面的 CRTP 和 Mixin 完全实现了多态性而没有运行时开销!
Templates give some possibility to do that.
模板只是基本的 C++ 工具。模板与 C++ 中的循环处于同一级别。在这种情况下说 "templates" 太宽泛了。
So, if we need class hierarchy, does it mean that we have to use it even it will force us to use less compile time checks?
如前所述,class 层次结构只是实现多态性任务的解决方案的一部分。多想想逻辑上的东西来实现,比如多态、序列化器、数据库等等,以及实现解决方案,比如虚函数、循环、堆栈、classes 等 "Compile time checks"?在大多数情况下,您不必自己编写 "checks"。一个简单的重载类似于编译时的 if/else,其中数据类型为 "checks"。所以开箱即用,不需要模板,也不需要 SFINAE。
Or we have to use templates to implement some sort of compile-time classes hierarchy even it will make our syntax much less compact and understandable
已经提到:模板代码可读! std::enable_if
作为一些手工制作的 SFINAE 东西更容易阅读,甚至两者都使用相同的 C++ 模板机制。如果您熟悉 c++ 20 个概念,您会发现在即将推出的 c++ 版本中很有可能编写更具可读性的模板代码。
现在很多关于 C++ 的演讲都是关于模板及其在编译时多态性实现中的用法;几乎不讨论虚函数和运行-时间多态性。
我们可以在很多情况下使用编译时多态性。因为它给了我们编译时检查而不是可能的 运行 时间多态性相关的 运行 时间错误,以及一些(通常微不足道的)性能优势,它看起来是当今使用最广泛的库比 运行-time 更喜欢编译时多态性。
但是,对我来说,与虚拟类型层次结构相比,使用 C++ 模板实现的编译时多态性导致的自我记录和可读代码要少得多。
我们可以回顾一下现实生活中的例子boost::iostreams
。它实现 stream
作为接受设备 class 作为参数的模板。这会导致特定功能的实现被分成许多 classes 和不同文件夹中的文件的情况,因此对此类代码的调查比流将形成 classes 具有虚拟功能的层次结构要复杂得多,例如我们有 Java 和 .NET Framework?编译时多态性在这里有什么好处?文件流是读取和写入文件的东西,流是读取和写入(任何东西)的东西,它是类型层次结构的 class 典型示例,所以为什么不使用单个 FileStream
class重载一些受保护的功能,而不是将语义统一的功能划分到不同的文件和 classes?
另一个例子是boost::process::child
class。它使用模板化构造函数来设置标准 i/o 和其他过程参数。它没有很好的文档记录,并且从这个函数原型中看不出这个模板将接受什么格式的参数;类似于 SetStandardOutput
的成员函数的实现将更好地自我记录并导致更快的编译时间,那么在这里使用模板有什么好处呢?同样,我将此实现与此处的 .NET Framework 进行比较。对于类似于 SetStandardOutput
的成员函数,阅读单个头文件就足以了解如何使用 class。对于 boost::process::child
的模板构造函数,我们必须读取许多小文件。
有很多类似的例子。出于任何原因,众所周知的开源库几乎从不使用虚拟 classes 层次结构,而是更喜欢像 boost
那样使用编译时多态性(主要是基于模板的)。
问题:在我们可以同时使用两者的情况下,我们必须选择什么(编译时多态性或 运行 时多态性)有明确的指导方针吗?
Generally speaking, in 90% of situation templates and virtual functions are interchangeable.
首先,我们需要澄清一下我们在说什么。如果你 "compare" 某些东西,它必须在某些标准中是等价的。我对您的陈述的理解不是将 virtual functions
与 templates
进行比较,而是在 polymorphism
!
在那种情况下你的例子没有很好地选择,而且 dynamic cast
更像是一个 "big hammer" 工具箱之外的东西,就好像我们谈论多态性一样。
你的 "template example" 不需要使用模板,因为你有简单的重载,根本不需要任何模板代码就可以使用!
如果我们谈论 polymorphism
和 c++
,我们首先选择 runtime-polymorphism
和 compile-time
多态性。对于这两者,我们都有标准的 C++ 解决方案。对于运行时,我们使用虚函数,对于编译时多态性,我们将 CRTP 作为典型实现,而不是模板作为通用术语!
are there any comments or recommendations from C++ committee or any other authoritative source, when we have to prefer ugly syntax of templates over much more understandable and compact virtual functions and inheritance syntax?
用惯了语法不难看!如果我们谈论用 SFINAE 实现东西,我们有一些难以理解的模板实例化规则,尤其是经常被误解的 deduced context
.
但在 C++20 中我们将有 concepts,它可以在大多数情况下取代 SFINAE,我认为这是一件好事。使用概念而不是 SFINAE 编写代码使其更具可读性、更易于维护并且更容易扩展新类型和 "rules".
Standard library if full of templates and has a very limited amount of virtual functions. Does it mean that we have to avoid virtual functions as much as possible and prefer templates always even if theirs syntax for some specific task is much less compact and much less understandable?
题目感觉你误解了C++的特性。模板允许使用 generic code
而 virtual functions
是实现 runtime polymorphism
的 C++ 工具。没有什么是 1:1 可比的。
在阅读您的示例代码时,我建议您重新考虑一下您的编码风格!
如果您想编写针对不同数据类型的函数,只需像在 "template" 示例中那样使用重载,但不要使用不需要的模板!
如果您想在同一代码中实现适用于不同数据类型的通用函数,请使用模板,如果特定数据类型需要一些特殊代码,请对选定的特定代码部分使用模板专门化.
如果您需要更多需要 SFINAE 的选择性模板代码,您应该开始使用 c++20 概念来实现。
如果要实现
polymorphism
决定使用run time
或compile time
多态性。如前所述,对于第一个,虚函数是实现它的标准 C++ 工具,而 CRTP 是第二个的标准解决方案之一。
而我对 "dynamic cast" 的个人体验是:避免它!这通常是您的设计出现问题的第一个提示。这不是一般规则,而是重新考虑设计的检查点。在极少数情况下,它是适合的工具。在 RTTI 中,并非对所有目标都可用,并且它有一些开销。在裸机 devices/embedded 系统上,您有时无法使用 RTTI 和异常。如果您的代码打算在您的域中用作 "platform",并且您有上述限制,请不要使用 RTTI!
编辑:来自评论的回答
So, for now, with C++ we can make classes hierarchy with run-time polymorphism only.
没有! CRTP 还构建 class 层次结构,但用于编译时多态性。但是解决方案完全不同,因为您没有 "common" 基础 class。但由于所有内容都在编译时解决,因此对公共基础 class 没有技术需求。您应该开始阅读有关 Mixins 的内容,也许在这里:What are Mixins (as a concept) and CRTP as one of the implementation methods: CRTP article wikipedia.
don't know how to implemented something similar to virtual functions without run-time overhead.
参见上面的 CRTP 和 Mixin 完全实现了多态性而没有运行时开销!
Templates give some possibility to do that.
模板只是基本的 C++ 工具。模板与 C++ 中的循环处于同一级别。在这种情况下说 "templates" 太宽泛了。
So, if we need class hierarchy, does it mean that we have to use it even it will force us to use less compile time checks?
如前所述,class 层次结构只是实现多态性任务的解决方案的一部分。多想想逻辑上的东西来实现,比如多态、序列化器、数据库等等,以及实现解决方案,比如虚函数、循环、堆栈、classes 等 "Compile time checks"?在大多数情况下,您不必自己编写 "checks"。一个简单的重载类似于编译时的 if/else,其中数据类型为 "checks"。所以开箱即用,不需要模板,也不需要 SFINAE。
Or we have to use templates to implement some sort of compile-time classes hierarchy even it will make our syntax much less compact and understandable
已经提到:模板代码可读! std::enable_if
作为一些手工制作的 SFINAE 东西更容易阅读,甚至两者都使用相同的 C++ 模板机制。如果您熟悉 c++ 20 个概念,您会发现在即将推出的 c++ 版本中很有可能编写更具可读性的模板代码。