普通模板在哪里结束,元模板从哪里开始

Where do normal templates end and meta templates begin?

本文关键字:开始 结束 在哪里      更新时间:2023-10-16

Jörg对这个问题的回答很好地描述了在数据上运行的"普通"模板(问题所指的泛型,也许是错误的泛型)和在程序上运行的元模板。Jörg然后明智地提到程序数据,所以它实际上都是一回事。也就是说,元模板仍然是一个不同的野兽。普通模板在哪里结束,元模板从哪里开始?

我能想出的最好的测试是模板的参数是否完全classtypename模板是"正常的",否则是元的。这个测试正确吗?

边界:具有逻辑行为的签名

好吧,在我看来,边界线是绘制的,模板的签名停止为产生运行时代码的简单签名,并成为显式或隐式逻辑的定义,将在编译时执行/解析。

一些例子和解释

常规模板,即仅具有类型名、类或可能的值类型模板参数,一旦在编译时实例化,就会生成可执行的 cpp 代码。

代码(重要)在编译时未执行

例如(非常简单且很可能是不切实际的示例,但解释了概念):

template<typename T>
T add(const T& lhs, const T& rhs) {
return(lhs + rhs);
}
template<>
std::string add<std::string>(
const std::string& lhs,
const std::string& rhs) {
return (lhs.append(rhs));
}
int main() {
double      result = add(1.0, 2.0); // 3.0
std::string s      = add("This is ", " the template specialization..."); 
}

编译完成后,根模板将用于实例化 double 类型的上述代码,但不会执行它。此外,专用化模板将实例化用于文本串联,但也: 不会在编译时执行。

但是,此示例:

#include <iostream>
#include <string>
#include <type_traits>
class INPCWithVoice {
void doSpeak() { ; }
};
class DefaultNPCWithVoice 
: public INPCWithVoice {
public:
inline std::string doSpeak() {
return "I'm so default, it hurts... But at least I can speak...";
}
}; 
class SpecialSnowflake
: public INPCWithVoice {
public:
inline std::string doSpeak() {
return "WEEEEEEEEEEEH~";   
}
};
class DefaultNPCWithoutVoice {
public:
inline std::string doSpeak() {
return "[...]";
}
};
template <typename TNPC>
static inline void speak(
typename std::enable_if<std::is_base_of<INPCWithVoice, TNPC>::value, TNPC>::type& npc) 
{
std::cout << npc.doSpeak() << std::endl;
};
int main()
{
DefaultNPCWithVoice    npc0 = DefaultNPCWithVoice();
SpecialSnowflake       npc1 = SpecialSnowflake();
DefaultNPCWithoutVoice npc2 = DefaultNPCWithoutVoice();
speak<DefaultNPCWithVoice>(npc0);
speak<SpecialSnowflake>(npc1);
// speak<DefaultNPCWithoutVoice>(npc2); // Won't compile, since DefaultNPCWithoutVoice does not derive from INPCWithVoice
}

此示例显示了模板元编程(实际上是一个简单的示例... 这里发生的情况是,"speak"函数有一个模板化参数,如果在编译时解析并衰减到 TNPC,如果为其传递的类型派生自 INPCWithVoice。

这反过来意味着,如果没有,模板将没有实例化的候选对象,并且编译已经失败。 查找SFINAE以获取此技术: http://eli.thegreenplace.net/2014/sfinae-and-enable_if/

此时,在编译时执行了一些逻辑,整个程序一旦链接到可执行文件/库,就会完全解决

另一个很好的例子是:https://akrzemi1.wordpress.com/2012/03/19/meta-functions-in-c11/

在这里,您可以看到阶乘函数的模板元编程实现,证明如果元模板衰减为常数,即使是字节码也可以完全等于固定值的使用。

最终确定示例:斐波那契

#include <iostream>
#include <string>
#include <type_traits>
template <intmax_t N>
static unsigned int fibonacci() {
return fibonacci<N - 1>() + fibonacci<N - 2>();     
}
template <>
unsigned int fibonacci<1>() {
return 1;   
}
template <>
unsigned int fibonacci<2>() {
return fibonacci<1>();    
}
template <intmax_t MAX>
static void Loop() {
std::cout << "Fibonacci at " << MAX << ": " << fibonacci<MAX>() << std::endl;
Loop<MAX - 1>();
}
template <>
void Loop<0>() {
std::cout << "End" << std::endl;    
}
int main()
{
Loop<10>();
}

此代码仅为位置 N 处的斐波那契数列实现标量模板参数模板元编程。 此外,它还显示了从 10 到 0 的循环计数的编译时!

最后

我希望这能澄清一些事情。

但请记住:循环和斐波那契示例为每个索引实例化上述模板!!

因此,存在可怕的冗余和二进制膨胀!!

我自己不是专家,我确信 stackoverflow 上有一个模板元编程功夫大师,他可以附加任何缺少的必要信息。

尝试区分和定义术语

让我们首先尝试粗略地定义这些术语。我从"编程"的足够好的定义开始,然后反复应用meta-的"通常"含义:

编程

编程会产生一个转换某些数据的程序。

int add(int value) { return value + 42; }

我刚刚编写了代码,该代码将导致一个程序将一些数据(整数)转换为其他一些数据。

模板(元编程)

元编程产生一个"程序",将某个程序转换为另一个程序。使用C++模板,没有有形的"程序",它是编译器工作的隐含部分。

template<typename T>
std::pair<T,T> two_of_them(T thing) {
return std::make_pair(thing, thing);
}

我只是编写了代码来指示编译器的行为类似于发出(代码)另一个程序的程序。

模板(元元编程?

编写元模板会产生一个"程序",该"程序"会产生一个"程序",从而产生一个程序。因此,在C++中,编写生成新模板的代码。(来自我的另一个答案:)

// map :: ([T] -> T) -> (T -> T) -> ([T] -> T)
//         "List"       "Mapping"   result "type" (also a "List")
// --------------------------------------------------------
template<template<typename...> class List,
template<typename> class Mapping>
struct map {
template<typename... Elements>
using type = List<typename Mapping<Elements>::type...>;
};

这是编译器如何将两个给定模板转换为新模板的描述。

可能的反对意见

看看其他答案,有人可能会争辩说,我的元编程示例不是"真正的"元编程,而是"泛型编程",因为它没有在"元"级别实现任何逻辑。但是,给出的编程示例可以被认为是"真正的"编程吗?它也没有实现任何逻辑,它是从数据到数据的简单映射,就像元编程示例实现从代码(auto p = two_of_them(42);)到代码(模板"填充"了正确的类型)的简单映射一样。

因此,IMO,添加条件(例如通过专业化)只会使模板更加复杂,但不会改变其性质。

您的测试

绝对没有。考虑:

template<typename X>
struct foo {
template<typename Y>
using type = X;
};

foo是具有单个typename参数的模板,但模板中的"结果"(名为foo::type...只是为了保持一致性),即"结果" - 无论给出什么参数 - 给foo的类型(从而给行为,该类型实现的程序)。

让我开始使用 dictionary.com 中的定义来回答

定义

元-

  1. 一个前缀添加到一个主题的名称中,并指定另一个分析原始主题但在更抽象、更高层次上的主题:元哲学;金属语言学。

  2. 添加到某物名称中的前缀,有意识地引用或评论其自己的主题或特征:元画 艺术家画画布。

模板编程最常用作在C++类型系统中表达关系的一种方式。因此,我认为可以公平地说,模板编程本质上利用了类型系统本身。

从这个角度来看,我们可以相当直接地应用上面给出的定义。模板编程和元(模板)编程之间的区别在于模板参数的处理和预期结果。

检查其参数的模板代码显然属于前者定义,而从模板参数创建新类型可以说属于后者。请注意,这还必须与代码对类型进行操作的意图相结合。

例子

让我们看一些例子:

实施标准::aligned_storage;

template<std::size_t Len, std::size_t Align /* default alignment not implemented */>
struct aligned_storage {
typedef struct {
alignas(Align) unsigned char data[Len];
} type;
};

此代码满足第二个条件,即类型std::aligned_storage用于创建另一个类型。我们可以通过创建一个包装器来使其更加清晰

template<typename T>
using storage_of = std::aligned_storage<sizeof(T), alignof(T)>::type;

现在我们满足上述两个条件,我们检查参数类型 T,提取它的大小和连接,然后我们使用该信息来构造一个依赖于我们的参数的新类型。这显然构成了元编程。

最初的std::aligned_storage不太清楚,但仍然相当普遍。我们以类型的形式提供结果,并且这两个参数都用于创建新类型。可以说,检查是在创建内部数组类型的type::data时发生的。

论证完整性的反例:

template<
class T,
class Container = std::vector<T>,
class Compare = std::less<typename Container::value_type>
> class priority_queue { /*Implementation defined implementation*/ };

在这里,您可能会有以下问题:

但是优先级队列不是也执行类型检查,例如检索底层容器或评估其迭代器的类型吗?

是的,确实如此,但目标是不同的。类型std::priority_queue本身并不构成元模板编程,因为它不利用信息在类型系统中进行操作。同时,以下内容将是元模板编程:

template<typename C>
using PriorityQueue = std::priority_queue<C>;

此处的目的是提供类型,而不是对数据本身的操作。当我们查看可以对每个代码进行的更改时,这一点会变得更加清晰。

我们可以更改std::priority_queue的实现,也许可以更改允许的操作。例如,支持更快的访问、附加操作或容器内位的紧凑存储。但所有这些都完全是为了实际的运行时功能,而不关心类型系统。

相比之下,看看我们可以对PriotityQueue做些什么。如果我们要选择不同的底层实现,例如,如果我们发现我们更喜欢Boost.Heap,或者我们无论如何都链接到Qt并希望选择它们的实现,那就是单行更改。这就是元编程的目的,我们在由其他类型形成的基于类型系统的参数中做出选择。

(元)模板签名

关于你的测试,正如我们上面看到的,storage_of只有typename参数,但很明显是元编程。如果你挖掘deaper,你会发现类型系统本身,带有模板,图灵完备。甚至不需要明确陈述任何积分变量,例如,我们可以轻松地用递归堆叠模板替换它们(即自然数的 Zermelo 构造)

using Z = void;
template<typename> struct Zermelo;
template<typename N> using Successor = Zermelo<N>;

在我看来,更好的测试是询问给定的实现是否具有运行时效果。如果模板结构或别名不包含任何仅在运行时发生效果的定义,则可能是模板元编程。

结语

当然,正常的模板编程可能会使用元模板编程。可以使用元模板编程来确定普通模板参数的属性。

例如,您可以选择不同的输出策略(假设某些元编程实现template<class Iterator> struct is_pointer_like;

template<class It> generateSomeData(It outputIterator) {
if constexpr(is_pointer_like<outputIterator>::value) {
generateFastIntoBuffer(static_cast<typename It::pointer> (std::addressof(*outputIterator));
} else {
generateOneByOne(outputIterator);
}
}

这构成了使用元模板编程实现的功能的模板编程。

普通模板在哪里结束,元模板从哪里开始?

当模板生成的代码依赖于编程的基本方面(例如分支和循环)时,您已经从普通模板越过了模板元编程的界限。

按照您链接的文章的描述:

常规函数

bool greater(int a, int b)
{
return (a > b);
}

仅处理一种类型的常规函数(暂时忽略隐式转换)。

函数模板(泛型编程)

template <typename T>
bool greater(T a, T b)
{
return (a > b);
}

通过使用函数模板,您已经创建了可应用于许多类型的泛型代码。但是,根据其用法,对于以空值结尾的 C 字符串,它可能不正确。

模板元编程

// Generic implementation
template <typename T>
struct greater_helper
{
bool operator(T a, T b) const
{
return (a > b);
}
};
template <typename T>
bool greater(T a, T b)
{
return greater_helper<T>().(a > b);
}
// Specialization for char const*
template <>
struct greater_helper<char const*>
{
bool operator(char const* a, char const* b) const
{
return (strcmp(a, b) > 0);
}
};

在这里,你写了代码,好像在说:

如果Tchar const*,请使用特殊函数。
对于T的所有其他值,请使用泛型函数。

现在,您已经跨越了普通模板到模板元编程的门槛。您已经引入了使用模板进行if-else分支的概念。